@barefootjs/jsx 0.31.1 → 0.31.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/interface.d.ts +11 -0
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/jsx-adapter.d.ts +92 -1
- package/dist/adapters/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/test-adapter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/css-layer-prefixer.d.ts +16 -0
- package/dist/css-layer-prefixer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/html-types.d.ts +19 -0
- package/dist/html-types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1531 -1155
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/props-event-handlers.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/props-extraction.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +6 -4
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-runtime/index.d.ts +2 -8
- package/dist/jsx-runtime/index.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/module-exports.d.ts +9 -1
- package/dist/module-exports.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +8 -3
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +40 -0
- package/dist/props-binding.d.ts.map +1 -0
- package/dist/relocate.d.ts +9 -0
- package/dist/relocate.d.ts.map +1 -1
- package/dist/scope/binding-scope.d.ts +179 -0
- package/dist/scope/binding-scope.d.ts.map +1 -0
- package/dist/ssr-defaults.d.ts +43 -0
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/template-parts.d.ts +53 -0
- package/dist/template-parts.d.ts.map +1 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/adapter-output.test.ts +8 -4
- package/src/__tests__/aliased-destructured-prop-csr.test.ts +112 -0
- package/src/__tests__/binding-scope-preamble-shadowing.test.ts +115 -0
- package/src/__tests__/binding-scope-ratchet.test.ts +194 -0
- package/src/__tests__/binding-scope.test.ts +200 -0
- package/src/__tests__/css-layer-prefixer.test.ts +72 -0
- package/src/__tests__/form-control-value-ssr.test.ts +48 -3
- package/src/__tests__/let-type-annotation.test.ts +208 -0
- package/src/__tests__/memo-deps-comments.test.ts +99 -0
- package/src/__tests__/multi-return-sibling-diagnostic.test.ts +241 -0
- package/src/__tests__/ssr-defaults.test.ts +124 -1
- package/src/__tests__/staged-ir/08-relocate-unit.test.ts +1 -0
- package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +1 -0
- package/src/adapters/interface.ts +11 -0
- package/src/adapters/jsx-adapter.ts +295 -5
- package/src/adapters/test-adapter.ts +13 -10
- package/src/analyzer.ts +70 -11
- package/src/compiler.ts +119 -18
- package/src/css-layer-prefixer.ts +80 -24
- package/src/errors.ts +18 -0
- package/src/html-types.ts +24 -0
- package/src/index.ts +11 -1
- package/src/ir-to-client-js/collect-elements.ts +4 -1
- package/src/ir-to-client-js/emit-reactive.ts +4 -2
- package/src/ir-to-client-js/phases/props-event-handlers.ts +4 -3
- package/src/ir-to-client-js/phases/props-extraction.ts +7 -4
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +6 -3
- package/src/ir-to-client-js/utils.ts +5 -24
- package/src/jsx-runtime/index.ts +2 -7
- package/src/jsx-to-ir.ts +245 -107
- package/src/module-exports.ts +11 -2
- package/src/prop-rewrite.ts +26 -6
- package/src/props-binding.ts +70 -0
- package/src/relocate.ts +19 -2
- package/src/scope/binding-scope.ts +238 -0
- package/src/ssr-defaults.ts +70 -0
- package/src/template-parts.ts +81 -0
- package/src/types.ts +18 -0
package/src/prop-rewrite.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* stack, so `items.map((title) => title.a)` never turns into the
|
|
15
15
|
* syntactically invalid `.map((_p.title) => _p.title.a)` when `title`
|
|
16
16
|
* is also a prop. (Names bound by loop callbacks that ENCLOSE the
|
|
17
|
-
* expression are the caller's job — see the `ctx.
|
|
17
|
+
* expression are the caller's job — see the `ctx.scope` filter in
|
|
18
18
|
* `jsx-to-ir.ts`'s `rewriteBarePropRefs` wrapper, #2222.)
|
|
19
19
|
*/
|
|
20
20
|
|
|
@@ -135,7 +135,11 @@ export function collectAstPropRefs(
|
|
|
135
135
|
* Returns null when `text` does not parse cleanly as an expression —
|
|
136
136
|
* the caller falls back to the legacy regex rewrite.
|
|
137
137
|
*/
|
|
138
|
-
function applyScopedPropRefRewrite(
|
|
138
|
+
function applyScopedPropRefRewrite(
|
|
139
|
+
text: string,
|
|
140
|
+
propRefs: Set<string>,
|
|
141
|
+
propAliases?: ReadonlyMap<string, string>,
|
|
142
|
+
): string | null {
|
|
139
143
|
// Wrap in parens so object literals and arrows parse as expressions.
|
|
140
144
|
const prefix = '('
|
|
141
145
|
const sf = ts.createSourceFile('__bf_prop_rewrite.ts', `${prefix}${text}\n)`, ts.ScriptTarget.Latest, true)
|
|
@@ -149,11 +153,15 @@ function applyScopedPropRefRewrite(text: string, propRefs: Set<string>): string
|
|
|
149
153
|
const start = n.getStart(sf) - prefix.length
|
|
150
154
|
const end = n.getEnd() - prefix.length
|
|
151
155
|
if (start < 0 || end > text.length) return
|
|
156
|
+
// `_p` is always keyed by the caller-facing name (`sourceName ?? name`
|
|
157
|
+
// — #2524 CSR half); the local binding (`n.text`) only survives on the
|
|
158
|
+
// left of a shorthand expansion.
|
|
159
|
+
const callerKey = propAliases?.get(n.text) ?? n.text
|
|
152
160
|
if (parent && ts.isShorthandPropertyAssignment(parent) && parent.name === n) {
|
|
153
|
-
edits.push({ start, end, replacement: `${n.text}: ${PROPS_PARAM}.${
|
|
161
|
+
edits.push({ start, end, replacement: `${n.text}: ${PROPS_PARAM}.${callerKey}` })
|
|
154
162
|
return
|
|
155
163
|
}
|
|
156
|
-
edits.push({ start, end, replacement: `${PROPS_PARAM}.${
|
|
164
|
+
edits.push({ start, end, replacement: `${PROPS_PARAM}.${callerKey}` })
|
|
157
165
|
})
|
|
158
166
|
|
|
159
167
|
if (edits.length === 0) return text
|
|
@@ -177,11 +185,14 @@ function applyScopedPropRefRewrite(text: string, propRefs: Set<string>): string
|
|
|
177
185
|
export function applyRegexPropRefRewrite(
|
|
178
186
|
text: string,
|
|
179
187
|
propRefs: Iterable<string>,
|
|
188
|
+
propAliases?: ReadonlyMap<string, string>,
|
|
180
189
|
): string {
|
|
181
190
|
const { protect, restore } = createTemplateAwareStringProtector()
|
|
182
191
|
let result = protect(text)
|
|
183
192
|
|
|
184
193
|
for (const propName of propRefs) {
|
|
194
|
+
// `_p` is always keyed by the caller-facing name (#2524 CSR half).
|
|
195
|
+
const callerKey = propAliases?.get(propName) ?? propName
|
|
185
196
|
const pattern = new RegExp(`(?<!${PROPS_PARAM}\\.)(?<!['"\\w.-])\\b${propName}\\b(?![a-zA-Z0-9_$])`, 'g')
|
|
186
197
|
result = result.replace(pattern, (match, offset, str) => {
|
|
187
198
|
// Skip object literal keys: preceded by { or , and followed by :
|
|
@@ -190,7 +201,7 @@ export function applyRegexPropRefRewrite(
|
|
|
190
201
|
const before = str.slice(0, offset)
|
|
191
202
|
if (/[{,]\s*$/.test(before)) return match
|
|
192
203
|
}
|
|
193
|
-
return `${PROPS_PARAM}.${
|
|
204
|
+
return `${PROPS_PARAM}.${callerKey}`
|
|
194
205
|
})
|
|
195
206
|
}
|
|
196
207
|
|
|
@@ -209,12 +220,18 @@ export function applyRegexPropRefRewrite(
|
|
|
209
220
|
* `text` was produced by inlining a branch-local whose initializer
|
|
210
221
|
* references the prop). The rewrite only touches genuine value
|
|
211
222
|
* references, so passing an over-broad set is safe.
|
|
223
|
+
* @param propAliases - Local name → caller-facing key (`sourceName ?? name`)
|
|
224
|
+
* for aliased destructured props (`{ n: count }` → `count` → `n`).
|
|
225
|
+
* `_p` is always keyed by the caller-facing name (#2524 CSR half); a name
|
|
226
|
+
* absent from this map emits `_p.<name>` unchanged (the un-aliased case,
|
|
227
|
+
* where `sourceName ?? name` is an identity).
|
|
212
228
|
*/
|
|
213
229
|
export function rewriteBarePropRefs(
|
|
214
230
|
text: string,
|
|
215
231
|
node: ts.Node,
|
|
216
232
|
propNames: Set<string>,
|
|
217
233
|
extraPropRefs?: ReadonlySet<string>,
|
|
234
|
+
propAliases?: ReadonlyMap<string, string>,
|
|
218
235
|
): string | undefined {
|
|
219
236
|
// Walk AST to find which prop names are actually used as value references
|
|
220
237
|
const foundPropRefs = new Set<string>()
|
|
@@ -225,5 +242,8 @@ export function rewriteBarePropRefs(
|
|
|
225
242
|
}
|
|
226
243
|
}
|
|
227
244
|
if (foundPropRefs.size === 0) return undefined
|
|
228
|
-
return
|
|
245
|
+
return (
|
|
246
|
+
applyScopedPropRefRewrite(text, foundPropRefs, propAliases) ??
|
|
247
|
+
applyRegexPropRefRewrite(text, foundPropRefs, propAliases)
|
|
248
|
+
)
|
|
229
249
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import type { ParamInfo } from './types.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Authoritative IdentifierName classification for a destructure-pattern
|
|
6
|
+
* property key, built on TS's own `isIdentifierStart` / `isIdentifierPart`
|
|
7
|
+
* primitives (Unicode-aware, stays aligned with what TS itself accepts as
|
|
8
|
+
* a bare property key). Mirrors the `isIdent` precedent in
|
|
9
|
+
* `jsx-to-ir.ts` (#1244) — a source key like `data-key` or `aria-label`
|
|
10
|
+
* can't be emitted as a bare `key: local` destructure and must be quoted
|
|
11
|
+
* (`"data-key": local`).
|
|
12
|
+
*/
|
|
13
|
+
export function isIdentifierName(key: string): boolean {
|
|
14
|
+
if (key.length === 0) return false
|
|
15
|
+
for (let i = 0; i < key.length; ) {
|
|
16
|
+
const cp = key.codePointAt(i)!
|
|
17
|
+
const ok = i === 0
|
|
18
|
+
? ts.isIdentifierStart(cp, ts.ScriptTarget.Latest)
|
|
19
|
+
: ts.isIdentifierPart(cp, ts.ScriptTarget.Latest)
|
|
20
|
+
if (!ok) return false
|
|
21
|
+
i += cp > 0xFFFF ? 2 : 1
|
|
22
|
+
}
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The single destructure-binding renderer for a props param, shared by
|
|
28
|
+
* every JSX-runtime SSR adapter (Hono, TestAdapter). The caller-facing
|
|
29
|
+
* key is `sourceName ?? name` (ParamInfo's own rule) — `name` is only
|
|
30
|
+
* ever the LOCAL binding. Emits the plain shorthand when they match
|
|
31
|
+
* (byte-identical to the pre-rename-aware form); emits a `key: local`
|
|
32
|
+
* rename otherwise (b4f5075). This also covers the `class` → `className`
|
|
33
|
+
* rename: a source prop literally named `class` can only reach
|
|
34
|
+
* `propsParams` via an aliased destructure (`{ class: className }` —
|
|
35
|
+
* `class` is a reserved word, so it can never be an un-aliased binding),
|
|
36
|
+
* which sets `sourceName: 'class'` and takes the rename branch
|
|
37
|
+
* (`class: className`), not a bare `className`.
|
|
38
|
+
*
|
|
39
|
+
* One exported implementation, two consumers, zero drift — the
|
|
40
|
+
* hono/test-adapter pair carrying private copies is exactly the
|
|
41
|
+
* lockstep-rule duplication #2460/#2524 were about.
|
|
42
|
+
*/
|
|
43
|
+
export function propsDestructureBinding(p: ParamInfo): string {
|
|
44
|
+
const callerKey = p.sourceName ?? p.name
|
|
45
|
+
const localName = p.name
|
|
46
|
+
const binding = callerKey === localName
|
|
47
|
+
? localName
|
|
48
|
+
: `${isIdentifierName(callerKey) ? callerKey : JSON.stringify(callerKey)}: ${localName}`
|
|
49
|
+
return p.defaultValue ? `${binding} = ${p.defaultValue}` : binding
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Local-name → caller-facing-key map for prop-reference rewrites —
|
|
54
|
+
* entries only for `ParamInfo`s that actually rename (`sourceName` set,
|
|
55
|
+
* see its docstring in `types.ts`). `_p` is always keyed by the
|
|
56
|
+
* caller-facing name (#2524 CSR half); an un-aliased prop leaves no
|
|
57
|
+
* entry, so `map?.get(name) ?? name` degrades to an identity there.
|
|
58
|
+
* Returns `undefined` when nothing renames, so callers can
|
|
59
|
+
* short-circuit.
|
|
60
|
+
*/
|
|
61
|
+
export function buildPropAliasMap(params: readonly ParamInfo[]): Map<string, string> | undefined {
|
|
62
|
+
let map: Map<string, string> | undefined
|
|
63
|
+
for (const p of params) {
|
|
64
|
+
if (p.sourceName) {
|
|
65
|
+
if (!map) map = new Map()
|
|
66
|
+
map.set(p.name, p.sourceName)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return map
|
|
70
|
+
}
|
package/src/relocate.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { Scope, BindingKind, IRMetadata } from './types.ts'
|
|
|
15
15
|
import { isVisibleIn } from './types.ts'
|
|
16
16
|
import type { AnalyzerContext } from './analyzer-context.ts'
|
|
17
17
|
import { PROPS_PARAM } from './ir-to-client-js/utils.ts'
|
|
18
|
+
import { buildPropAliasMap } from './props-binding.ts'
|
|
18
19
|
import type {
|
|
19
20
|
TemplatePrimitiveRegistry,
|
|
20
21
|
TemplateCallAcceptor,
|
|
@@ -42,6 +43,15 @@ export interface RelocateEnv {
|
|
|
42
43
|
* `TransformContext._destructuredPropNames`.
|
|
43
44
|
*/
|
|
44
45
|
propsForLift: Set<string>
|
|
46
|
+
/**
|
|
47
|
+
* Local prop name → caller-facing key (`sourceName ?? name`), entries
|
|
48
|
+
* only for `ParamInfo`s that rename (`{ n: count }` → `count` → `n`).
|
|
49
|
+
* `_p` is always keyed by the caller-facing name (#2524 CSR half) — the
|
|
50
|
+
* `lift-to-prop` action reads this so `count` lifts to `_p.n`, not
|
|
51
|
+
* `_p.count`. A name absent from this map is un-aliased, so
|
|
52
|
+
* `propSourceNames.get(name) ?? name` degrades to an identity there.
|
|
53
|
+
*/
|
|
54
|
+
propSourceNames: ReadonlyMap<string, string>
|
|
45
55
|
/**
|
|
46
56
|
* Name of the props parameter (e.g. `props`). Used to detect
|
|
47
57
|
* `props.X` member access at lift sites — those are not free refs
|
|
@@ -179,8 +189,10 @@ function decideAction(
|
|
|
179
189
|
if (env.propsObjectName !== null && name === env.propsObjectName) {
|
|
180
190
|
return { action: 'lift-to-prop', rewrittenAs: PROPS_PARAM }
|
|
181
191
|
}
|
|
182
|
-
// Lift `name` → `_p
|
|
183
|
-
|
|
192
|
+
// Lift `name` → `_p.<caller-facing key>` — `_p` is always keyed by
|
|
193
|
+
// the caller-facing name (#2524 CSR half), not the local binding.
|
|
194
|
+
const callerKey = env.propSourceNames.get(name) ?? name
|
|
195
|
+
return { action: 'lift-to-prop', rewrittenAs: `${PROPS_PARAM}.${callerKey}` }
|
|
184
196
|
}
|
|
185
197
|
|
|
186
198
|
if ((kind === 'init-local' || kind === 'sub-init-local') && toScope === 'template') {
|
|
@@ -860,6 +872,10 @@ function buildRelocateEnvFromFields(src: EnvFields): RelocateEnv {
|
|
|
860
872
|
if (kind === 'prop') propsForLift.add(name)
|
|
861
873
|
}
|
|
862
874
|
|
|
875
|
+
// propSourceNames: local prop name → caller-facing key, entries only
|
|
876
|
+
// for `ParamInfo`s that actually rename. See `RelocateEnv.propSourceNames`.
|
|
877
|
+
const propSourceNames = buildPropAliasMap(src.propsParams) ?? new Map<string, string>()
|
|
878
|
+
|
|
863
879
|
// aliasTargets (#2069 R2): one-hop alias resolution table for
|
|
864
880
|
// `isCallAcceptedByAdapter`. A const whose FINAL resolved binding kind
|
|
865
881
|
// is `init-local` or `module-local` (i.e. not a signal/memo/prop-alias
|
|
@@ -884,6 +900,7 @@ function buildRelocateEnvFromFields(src: EnvFields): RelocateEnv {
|
|
|
884
900
|
bindings,
|
|
885
901
|
inlinable: new Map(), // populated by compute-inlinability after analyzer runs
|
|
886
902
|
propsForLift,
|
|
903
|
+
propSourceNames,
|
|
887
904
|
propsObjectName,
|
|
888
905
|
allowFallback: true,
|
|
889
906
|
aliasTargets,
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `BindingScope`: the one shared, immutable, stack-shaped model of "names
|
|
3
|
+
* bound by a loop callback" (#2482 Stage 0). Six independent ad-hoc
|
|
4
|
+
* mechanisms across the compiler currently answer this same question —
|
|
5
|
+
* `ctx.loopParams` (a mutated `Set<string>` in `jsx-to-ir.ts`),
|
|
6
|
+
* `collectLoopBoundNames` (`adapters/loop-bound-names.ts`),
|
|
7
|
+
* `resolveStaticLoopSource`'s `isNameShadowed` callback
|
|
8
|
+
* (`static-literal.ts`), and others — each reimplementing the same
|
|
9
|
+
* item/index/destructure/preamble-local bookkeeping with its own bugs and
|
|
10
|
+
* its own blind spots. This module is the single door those mechanisms
|
|
11
|
+
* migrate onto in later stages (Stage 0 only ships the service + tests;
|
|
12
|
+
* NO call site is migrated yet).
|
|
13
|
+
*
|
|
14
|
+
* Immutability is the point, not an incidental style choice:
|
|
15
|
+
* `ctx.loopParams` is `.add`/`.delete`-mutated as `jsx-to-ir.ts` walks in
|
|
16
|
+
* and back out of nested loops, so a caller that forgets (or races) a
|
|
17
|
+
* `.delete()` — or that holds a reference to the "current" set across a
|
|
18
|
+
* push/pop it didn't expect — silently observes the WRONG scope. A
|
|
19
|
+
* restore-bug of that shape is impossible by construction here:
|
|
20
|
+
* `enterLoopRow`/`enterCallback` never mutate `this`, they return a NEW
|
|
21
|
+
* `BindingScope` whose parent is untouched, so holding an old reference
|
|
22
|
+
* always sees the scope as it was, and there is no delete step to forget.
|
|
23
|
+
*
|
|
24
|
+
* Filter/sort callback params (`.filter(x => ...)`, `.sort((a, b) => ...)`,
|
|
25
|
+
* a nested arrow) are bound as `'callback'` frames via `enterCallback` —
|
|
26
|
+
* never folded into a `'loop-row'` frame's bindings. They are a distinct
|
|
27
|
+
* scope-introduction shape (an inner function's own parameter list, not a
|
|
28
|
+
* row's item/index/destructure/preamble names) even though both end up
|
|
29
|
+
* "just names you can't resolve against component-level state."
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* How a name inside a `ScopeFrame` came to be bound — the row shapes
|
|
34
|
+
* (`'item'`/`'index'`/`'destructure'`/`'preamble'`) for `'loop-row'` frames,
|
|
35
|
+
* `'param'` for `'callback'` frames. One shared type because both frame
|
|
36
|
+
* kinds carry the same binding metadata.
|
|
37
|
+
*/
|
|
38
|
+
export type ScopeBindingSource = 'item' | 'index' | 'destructure' | 'preamble' | 'param'
|
|
39
|
+
|
|
40
|
+
export interface ScopeBinding {
|
|
41
|
+
readonly source: ScopeBindingSource
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ScopeFrame {
|
|
45
|
+
readonly kind: 'loop-row' | 'callback'
|
|
46
|
+
readonly bindings: ReadonlyMap<string, ScopeBinding>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Structural pick satisfied by BOTH `IRLoop` (`packages/jsx/src/types.ts`)
|
|
51
|
+
* and the client-JS `LoopCore` family (`packages/jsx/src/ir-to-client-js/types.ts`)
|
|
52
|
+
* without importing either — this module stays dependency-free and cannot
|
|
53
|
+
* form an import cycle with `types.ts` / `ir-to-client-js`.
|
|
54
|
+
*/
|
|
55
|
+
export interface LoopBindingSource {
|
|
56
|
+
readonly param: string
|
|
57
|
+
readonly index?: string | null
|
|
58
|
+
readonly paramBindings?: readonly { readonly name: string }[]
|
|
59
|
+
readonly preamble?: { readonly declaredNames: readonly string[] } | null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A stack of `ScopeFrame`s, innermost frame at index 0 of the internal
|
|
64
|
+
* array (i.e. `frames[0]` is what `enterLoopRow`/`enterCallback` most
|
|
65
|
+
* recently pushed). `lookup`'s `depth` counts from `frames[0]`, so depth 0
|
|
66
|
+
* always means "the innermost frame," independent of how many ancestor
|
|
67
|
+
* frames exist.
|
|
68
|
+
*/
|
|
69
|
+
export class BindingScope {
|
|
70
|
+
static readonly EMPTY: BindingScope = new BindingScope([])
|
|
71
|
+
|
|
72
|
+
private constructor(private readonly frames: readonly ScopeFrame[]) {}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Child scope with a new `'loop-row'` frame for one loop's per-item
|
|
76
|
+
* bindings. Parent (`this`) is not mutated; the returned scope is a
|
|
77
|
+
* NEW object with `frames = [newFrame, ...this.frames]`.
|
|
78
|
+
*
|
|
79
|
+
* Binding semantics mirror `jsx-to-ir.ts`'s `ctx.loopParams` add site
|
|
80
|
+
* EXACTLY (verified against lines ~4320-4345 and the matching delete
|
|
81
|
+
* site ~4695-4710 of `packages/jsx/src/jsx-to-ir.ts`):
|
|
82
|
+
*
|
|
83
|
+
* - When `loop.paramBindings` is non-empty (a destructured callback
|
|
84
|
+
* param, e.g. `.map(({ id, name }) => ...)`), each `paramBindings[i].name`
|
|
85
|
+
* is bound with source `'destructure'` and the raw `param` text
|
|
86
|
+
* (which for a destructured callback holds the ORIGINAL pattern
|
|
87
|
+
* source, e.g. `"{ id, name }"`, not a usable identifier) is NOT
|
|
88
|
+
* bound. This matches `jsx-to-ir.ts`:
|
|
89
|
+
* `if (paramBindings) { for (const b of paramBindings) ctx.loopParams.add(b.name) }`
|
|
90
|
+
* — the `else` branch (`ctx.loopParams.add(param)`) is skipped
|
|
91
|
+
* entirely when `paramBindings` is present.
|
|
92
|
+
* - Otherwise (a plain identifier param, e.g. `.map(item => ...)`),
|
|
93
|
+
* `param` itself is bound with source `'item'`.
|
|
94
|
+
* - `index` (the second callback param, e.g. `.map((item, i) => ...)`)
|
|
95
|
+
* is bound with source `'index'` when non-null/non-undefined.
|
|
96
|
+
* - Every name in `preamble.declaredNames` (a `.map()` callback's
|
|
97
|
+
* pre-return `const`/`let`/`function` locals, #2447) is bound with
|
|
98
|
+
* source `'preamble'`.
|
|
99
|
+
*
|
|
100
|
+
* NOTE on a sibling mechanism this method does NOT mirror:
|
|
101
|
+
* `adapters/loop-bound-names.ts`'s `collectLoopBoundNames` adds BOTH
|
|
102
|
+
* `node.param` AND every `paramBindings[i].name` unconditionally
|
|
103
|
+
* (never skipping `param` in the destructured case) — a deliberately
|
|
104
|
+
* coarser, over-inclusive collection used only to subtract names from
|
|
105
|
+
* a flat string-typing Set (safe to over-exclude there). This method
|
|
106
|
+
* follows the precise `jsx-to-ir.ts` `ctx.loopParams` semantics, since
|
|
107
|
+
* that is the mechanism actually doing scope-shadowed name RESOLUTION
|
|
108
|
+
* (the behavior `BindingScope` replaces), not coarse exclusion.
|
|
109
|
+
*/
|
|
110
|
+
enterLoopRow(loop: LoopBindingSource): BindingScope {
|
|
111
|
+
const bindings = new Map<string, ScopeBinding>()
|
|
112
|
+
if (loop.paramBindings && loop.paramBindings.length > 0) {
|
|
113
|
+
for (const b of loop.paramBindings) bindings.set(b.name, { source: 'destructure' })
|
|
114
|
+
} else {
|
|
115
|
+
bindings.set(loop.param, { source: 'item' })
|
|
116
|
+
}
|
|
117
|
+
if (loop.index != null) bindings.set(loop.index, { source: 'index' })
|
|
118
|
+
for (const name of loop.preamble?.declaredNames ?? []) bindings.set(name, { source: 'preamble' })
|
|
119
|
+
|
|
120
|
+
const frame: ScopeFrame = { kind: 'loop-row', bindings }
|
|
121
|
+
return new BindingScope([frame, ...this.frames])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Child scope with a new `'callback'` frame binding `params` (a filter
|
|
126
|
+
* predicate's `x`, a sort comparator's `(a, b)`, or a nested arrow's
|
|
127
|
+
* parameter list) with source `'param'`. Parent is not mutated.
|
|
128
|
+
*/
|
|
129
|
+
enterCallback(params: readonly string[]): BindingScope {
|
|
130
|
+
const bindings = new Map<string, ScopeBinding>()
|
|
131
|
+
for (const name of params) bindings.set(name, { source: 'param' })
|
|
132
|
+
const frame: ScopeFrame = { kind: 'callback', bindings }
|
|
133
|
+
return new BindingScope([frame, ...this.frames])
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Innermost-first membership check across every frame in the stack. */
|
|
137
|
+
isBound(name: string): boolean {
|
|
138
|
+
for (const frame of this.frames) {
|
|
139
|
+
if (frame.bindings.has(name)) return true
|
|
140
|
+
}
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolves `name` against the frame stack innermost-first. `depth 0`
|
|
146
|
+
* means the innermost (most recently entered) frame; `null` when `name`
|
|
147
|
+
* is not bound in any frame.
|
|
148
|
+
*/
|
|
149
|
+
lookup(name: string): { readonly depth: number; readonly frame: ScopeFrame; readonly binding: ScopeBinding } | null {
|
|
150
|
+
for (let depth = 0; depth < this.frames.length; depth++) {
|
|
151
|
+
const frame = this.frames[depth]
|
|
152
|
+
const binding = frame.bindings.get(name)
|
|
153
|
+
if (binding) return { depth, frame, binding }
|
|
154
|
+
}
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Union of every frame's bound names (every `ScopeBindingSource`), for
|
|
160
|
+
* migration interop with legacy `Set<string>`-shaped consumers (e.g.
|
|
161
|
+
* `collectLoopBoundNames`'s return type) as later stages migrate them
|
|
162
|
+
* onto `BindingScope`.
|
|
163
|
+
*
|
|
164
|
+
* This is the SHADOW-GUARD query — see {@link valueBoundNames} for the
|
|
165
|
+
* other consumer class and why the two must not be conflated.
|
|
166
|
+
*/
|
|
167
|
+
boundNames(): ReadonlySet<string> {
|
|
168
|
+
if (this.boundNamesCache) return this.boundNamesCache
|
|
169
|
+
const names = new Set<string>()
|
|
170
|
+
for (const frame of this.frames) {
|
|
171
|
+
for (const name of frame.bindings.keys()) names.add(name)
|
|
172
|
+
}
|
|
173
|
+
this.boundNamesCache = names
|
|
174
|
+
return names
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Both name queries are hot (shadow guards, slot/reactivity classifiers,
|
|
178
|
+
// binding-env memo keying) and the scope is immutable, so each computes
|
|
179
|
+
// once per instance. Callers receive the cached set as ReadonlySet —
|
|
180
|
+
// never mutate it.
|
|
181
|
+
private boundNamesCache: ReadonlySet<string> | undefined
|
|
182
|
+
private valueBoundNamesCache: ReadonlySet<string> | undefined
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Union of names bound via `'item'`/`'index'`/`'destructure'` sources
|
|
186
|
+
* only — the loop row's own per-item identity — excluding `'preamble'`
|
|
187
|
+
* (a `.map()` callback's pre-return `const`/`let`/`function` locals,
|
|
188
|
+
* #2447) and `'param'` (an `enterCallback` frame's filter/sort/nested-
|
|
189
|
+
* arrow parameters).
|
|
190
|
+
*
|
|
191
|
+
* `BindingScope` has exactly two consumer classes, and conflating them
|
|
192
|
+
* is the #2482 Stage 1a Commit 2 regression this split exists to
|
|
193
|
+
* prevent (a `ctx.scope`-wide preamble merge flipped `tag-cloud` and
|
|
194
|
+
* `preamble-cells` conformance fixtures before this method existed):
|
|
195
|
+
*
|
|
196
|
+
* - SHADOW GUARDS (`tryResolveTemplateSpanFromConst`,
|
|
197
|
+
* `tryResolveIdentifierAsTemplateLiteral`, `rewriteBarePropRefs`
|
|
198
|
+
* in `jsx-to-ir.ts`) ask "is this name resolved to SOMETHING in
|
|
199
|
+
* this scope, so an outer const/prop of the same name must not be
|
|
200
|
+
* substituted here at this transform position" — every source
|
|
201
|
+
* qualifies, including a preamble local shadowing a module const.
|
|
202
|
+
* These call `isBound` / `boundNames()`.
|
|
203
|
+
* - REACTIVITY / SLOT-ID CLASSIFIERS (`referencesLoopParam`,
|
|
204
|
+
* `hasReactiveAttributes`, and the `BindingEnvironment.loopParams`
|
|
205
|
+
* feed built from `makeBindingEnv`, all in `jsx-to-ir.ts`) ask
|
|
206
|
+
* "does this expression read a value that changes per row and so
|
|
207
|
+
* needs its own patchable slot" — a preamble local already gets
|
|
208
|
+
* ITS OWN dedicated slot/region-patch machinery
|
|
209
|
+
* (`preambleRegions` / `markPreambleAttrSlots`, #2447), so folding
|
|
210
|
+
* it into this classification double-counts it. Worse: widening a
|
|
211
|
+
* text child's `reactive` flag this way is read by
|
|
212
|
+
* `hasDynamicContent` to decide whether the loop ROW's own root
|
|
213
|
+
* element needs a slot — an unrelated, narrower decision that must
|
|
214
|
+
* not move just because a preamble local is now scope-visible.
|
|
215
|
+
* These call `valueBoundNames()`.
|
|
216
|
+
*/
|
|
217
|
+
valueBoundNames(): ReadonlySet<string> {
|
|
218
|
+
if (this.valueBoundNamesCache) return this.valueBoundNamesCache
|
|
219
|
+
const names = new Set<string>()
|
|
220
|
+
for (const frame of this.frames) {
|
|
221
|
+
for (const [name, binding] of frame.bindings) {
|
|
222
|
+
if (binding.source === 'item' || binding.source === 'index' || binding.source === 'destructure') {
|
|
223
|
+
names.add(name)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
this.valueBoundNamesCache = names
|
|
228
|
+
return names
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Drop-in for `resolveStaticLoopSource`'s `opts.isNameShadowed`
|
|
233
|
+
* (`packages/jsx/src/static-literal.ts:112-128`).
|
|
234
|
+
*/
|
|
235
|
+
asShadowPredicate(): (name: string) => boolean {
|
|
236
|
+
return (name: string) => this.isBound(name)
|
|
237
|
+
}
|
|
238
|
+
}
|
package/src/ssr-defaults.ts
CHANGED
|
@@ -62,6 +62,76 @@ export interface SsrDefault {
|
|
|
62
62
|
isRestProps?: boolean
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* TS twin of the runtime `derive*FromDefaults` family that ships in every
|
|
67
|
+
* OTHER SSR runtime port (Ruby's `BarefootJS::Context.derive_vars_from_defaults`
|
|
68
|
+
* — `packages/adapter-erb/lib/barefoot_js.rb:337-360` — plus Python's
|
|
69
|
+
* `barefootjs.runtime._derive_stash_from_defaults`, PHP's
|
|
70
|
+
* `Barefoot\BarefootJS::deriveStashFromDefaults`, Perl's
|
|
71
|
+
* `BarefootJS::_derive_stash_from_defaults`, and Rust's
|
|
72
|
+
* `barefootjs::manifest::derive_stash_from_defaults`). TypeScript had no such
|
|
73
|
+
* function — every adapter-tests conformance harness (and 3 production
|
|
74
|
+
* integration sites) either discarded `SsrDefault.propName` outright or keyed
|
|
75
|
+
* its seeding loop off the LOCAL template-var name instead of the
|
|
76
|
+
* caller-facing prop key, which is exactly the #2157 defect class (and its
|
|
77
|
+
* #2524 SSR-seeding recurrence) restated at
|
|
78
|
+
* `packages/adapter-erb/src/test-render.ts:276-287`. Any harness or
|
|
79
|
+
* integration deriving template-stash vars from an `extractSsrDefaults(...)`
|
|
80
|
+
* map MUST route through this function (or its runtime-language twin, when
|
|
81
|
+
* one is reachable) instead of hand-flattening `SsrDefault.value` and
|
|
82
|
+
* merging raw caller props over it — that flattening is precisely what
|
|
83
|
+
* silently drops the rename.
|
|
84
|
+
*
|
|
85
|
+
* Semantics (mirrors `derive_vars_from_defaults`'s observable behavior,
|
|
86
|
+
* including its edge cases — though not always the identical mechanism;
|
|
87
|
+
* e.g. Python's `_derive_stash_from_defaults` checks `props.get(prop_name)
|
|
88
|
+
* is not None` with no separate `in` membership test, relying on `dict.get`
|
|
89
|
+
* defaulting a missing key to `None` — behaviorally identical to this
|
|
90
|
+
* function's explicit `propName in props && props[propName] != null` for a
|
|
91
|
+
* plain dict/object, just expressed differently):
|
|
92
|
+
* - A non-object entry (a bare JSON value some callers may still pass,
|
|
93
|
+
* e.g. a manifest round-tripped through a generic JSON domain) is used
|
|
94
|
+
* AS-IS.
|
|
95
|
+
* - `isRestProps` entries: prefer `props[<this entry's own key>]` when the
|
|
96
|
+
* caller supplied one (checked via `in`, so an explicit `undefined` /
|
|
97
|
+
* `null` value still counts as "supplied" — the rest bag is a
|
|
98
|
+
* caller-assembled aggregate, not a single scalar with a meaningful
|
|
99
|
+
* "absent" state), else the static `value` fallback (normally `{}`).
|
|
100
|
+
* - Otherwise: prefer `props[propName]` when `propName` is set AND the
|
|
101
|
+
* caller supplied a NON-NULLISH (`!= null`, so `undefined` and `null`
|
|
102
|
+
* both fall through — mirrors every other port's "present and defined"
|
|
103
|
+
* check) value for it, else the static `value` fallback. `propName`-less
|
|
104
|
+
* entries (signal / memo locals) always use the static value — the
|
|
105
|
+
* caller cannot override them by construction.
|
|
106
|
+
*/
|
|
107
|
+
export function deriveStashFromDefaults(
|
|
108
|
+
defaults: Record<string, SsrDefault>,
|
|
109
|
+
props: Record<string, unknown>,
|
|
110
|
+
): Record<string, unknown> {
|
|
111
|
+
const extra: Record<string, unknown> = {}
|
|
112
|
+
for (const [name, d] of Object.entries(defaults)) {
|
|
113
|
+
if (d === null || typeof d !== 'object') {
|
|
114
|
+
// Defensive: every ENTRY `extractSsrDefaults` itself emits is always
|
|
115
|
+
// the `{ value, propName?, isRestProps? }` shape, but a caller may
|
|
116
|
+
// feed this a manifest round-tripped through a generic JSON domain
|
|
117
|
+
// (mirrors every runtime port's own `ref($d) eq 'HASH'` /
|
|
118
|
+
// `d.is_a?(Hash)` / `isinstance(d, dict)` guard).
|
|
119
|
+
extra[name] = d
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
if (d.isRestProps) {
|
|
123
|
+
extra[name] = name in props ? props[name] : d.value
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
if (d.propName !== undefined && d.propName in props && props[d.propName] != null) {
|
|
127
|
+
extra[name] = props[d.propName]
|
|
128
|
+
} else {
|
|
129
|
+
extra[name] = d.value
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return extra
|
|
133
|
+
}
|
|
134
|
+
|
|
65
135
|
const UNRESOLVED = Symbol('unresolved')
|
|
66
136
|
type EvalResult = unknown | typeof UNRESOLVED
|
|
67
137
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single renderer for a structured `template` variant's parts back into
|
|
3
|
+
* JS template-literal source.
|
|
4
|
+
*
|
|
5
|
+
* Three sites used to carry byte-identical copies of this loop — the IR-time
|
|
6
|
+
* component-prop collapse (`jsx-to-ir.ts`), the client-JS emitter
|
|
7
|
+
* (`ir-to-client-js/utils.ts`), and the JSX adapters' attribute renderer. They
|
|
8
|
+
* have to agree: the collapse's output is what a JSX adapter emits verbatim
|
|
9
|
+
* for a component prop, so a divergence between any two of them is a silent
|
|
10
|
+
* SSR/CSR mismatch. One door, three callers.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { IRTemplatePart } from './types.ts'
|
|
14
|
+
|
|
15
|
+
export interface TemplatePartsToJsOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Prefer each part's prop-rewritten projection (`templateValue` /
|
|
18
|
+
* `templateCondition` / `templateKey`, i.e. destructured props rewritten
|
|
19
|
+
* to `_p.X`) when present. Used by the client-JS / module-registration
|
|
20
|
+
* template emitters, which run outside the component's destructured scope.
|
|
21
|
+
*/
|
|
22
|
+
useTemplate?: boolean
|
|
23
|
+
/**
|
|
24
|
+
* Emit TypeScript type annotations. Only adapters whose output is
|
|
25
|
+
* type-checked as .tsx set this — see `JsxAdapterConfig.preserveTypes`.
|
|
26
|
+
* The neutral (untyped) form is what DSL adapters' expression pipelines
|
|
27
|
+
* and the client-JS bundle consume, so it must stay plain JS.
|
|
28
|
+
*/
|
|
29
|
+
typed?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Render one `lookup` part — `${MAP[KEY]}` structurally captured at IR time
|
|
34
|
+
* so DSL adapters can emit a switch — as the equivalent runtime indexed
|
|
35
|
+
* access against the resolved cases.
|
|
36
|
+
*
|
|
37
|
+
* Under `typed`, the inlined object literal is annotated
|
|
38
|
+
* `as Record<string, string>` (#2565). The IR's `key` is the TYPE-STRIPPED
|
|
39
|
+
* source text of the index expression, so a narrowing assertion written at
|
|
40
|
+
* the use site — `strokePaths[name as keyof typeof strokePaths]` — is already
|
|
41
|
+
* gone by the time the record's cases are folded in here. That leaves the
|
|
42
|
+
* literal's exact key set indexed by the binding's unnarrowed union, which
|
|
43
|
+
* fails TS7053 ("expression of type 'IconName' can't be used to index type
|
|
44
|
+
* '{ check: string; … }'") in any consumer that type-checks its compiled
|
|
45
|
+
* templates. Widening the literal to a string index signature restores the
|
|
46
|
+
* assertion's effect without reconstructing its text, which may name types
|
|
47
|
+
* the emitted template never declares (`keyof typeof strokePaths` where
|
|
48
|
+
* `strokePaths` was localised into a component body). Purely a type-level
|
|
49
|
+
* annotation — the runtime expression is identical either way.
|
|
50
|
+
*/
|
|
51
|
+
export function lookupPartToJsExpr(
|
|
52
|
+
part: Extract<IRTemplatePart, { type: 'lookup' }>,
|
|
53
|
+
opts?: TemplatePartsToJsOptions,
|
|
54
|
+
): string {
|
|
55
|
+
const key = (opts?.useTemplate && part.templateKey) ? part.templateKey : part.key
|
|
56
|
+
const obj = '{' + Object.entries(part.cases).map(
|
|
57
|
+
([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`
|
|
58
|
+
).join(', ') + '}'
|
|
59
|
+
const typed = opts?.typed ? ' as Record<string, string>' : ''
|
|
60
|
+
return `(${obj}${typed})[${key}]`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Convert a `template` variant's parts into a JS template-literal string. */
|
|
64
|
+
export function templatePartsToJsExpr(
|
|
65
|
+
parts: readonly IRTemplatePart[],
|
|
66
|
+
opts?: TemplatePartsToJsOptions,
|
|
67
|
+
): string {
|
|
68
|
+
let result = '`'
|
|
69
|
+
for (const part of parts) {
|
|
70
|
+
if (part.type === 'string') {
|
|
71
|
+
result += (opts?.useTemplate && part.templateValue) ? part.templateValue : part.value
|
|
72
|
+
} else if (part.type === 'ternary') {
|
|
73
|
+
const cond = (opts?.useTemplate && part.templateCondition) ? part.templateCondition : part.condition
|
|
74
|
+
result += `\${${cond} ? '${part.whenTrue}' : '${part.whenFalse}'}`
|
|
75
|
+
} else if (part.type === 'lookup') {
|
|
76
|
+
result += `\${${lookupPartToJsExpr(part, opts)}}`
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
result += '`'
|
|
80
|
+
return result
|
|
81
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -93,6 +93,16 @@ export interface ParamInfo {
|
|
|
93
93
|
type: TypeInfo
|
|
94
94
|
optional: boolean
|
|
95
95
|
defaultValue?: string
|
|
96
|
+
/**
|
|
97
|
+
* `defaultValue` parsed into a structured tree, mirroring `SignalInfo.parsed`
|
|
98
|
+
* (Roadmap A). Attached best-effort by the analyzer (`tsNodeToParsedExpr` on
|
|
99
|
+
* the binding element's own `initializer` node — no re-parse of the
|
|
100
|
+
* `defaultValue` text) so adapters can classify a destructure default's
|
|
101
|
+
* literal shape (`{ count = 3 }`, `{ ratio = 1.5 }`) from structure instead
|
|
102
|
+
* of regexing `defaultValue`. Absent when the shape isn't supported;
|
|
103
|
+
* consumers fall back to text-matching `defaultValue`.
|
|
104
|
+
*/
|
|
105
|
+
parsed?: ParsedExpr
|
|
96
106
|
/** When true, the default value contains an arrow function or function expression (computed from AST). */
|
|
97
107
|
defaultContainsArrow?: boolean
|
|
98
108
|
/** When true, the parameter is a rest spread (`...args`) — emit must prepend `...`. */
|
|
@@ -1872,6 +1882,14 @@ export interface ConstantInfo {
|
|
|
1872
1882
|
parsed?: ParsedExpr
|
|
1873
1883
|
/** Value with TypeScript type annotations preserved, for .tsx output */
|
|
1874
1884
|
typedValue?: string
|
|
1885
|
+
/**
|
|
1886
|
+
* The declaration's explicit type annotation, verbatim from source
|
|
1887
|
+
* (`node.type.getText()`), when the author wrote one. Distinct from
|
|
1888
|
+
* `type`, which is also populated by inference from the initializer —
|
|
1889
|
+
* emitters must only print THIS field, never an inferred type, onto a
|
|
1890
|
+
* declaration (#2589).
|
|
1891
|
+
*/
|
|
1892
|
+
typeAnnotation?: string
|
|
1875
1893
|
valueBranches?: string[]
|
|
1876
1894
|
declarationKind: 'const' | 'let'
|
|
1877
1895
|
isExported?: boolean
|