@barefootjs/jsx 0.31.4 → 0.31.6
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/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/loop-bound-names.d.ts +18 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -1
- package/dist/adapters/test-adapter.d.ts.map +1 -1
- package/dist/augment-inherited-props.d.ts +12 -2
- package/dist/augment-inherited-props.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/debug.d.ts.map +1 -1
- package/dist/free-refs.d.ts +11 -2
- package/dist/free-refs.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +773 -649
- package/dist/ir-to-client-js/client-only-elision.d.ts +11 -5
- package/dist/ir-to-client-js/client-only-elision.d.ts.map +1 -1
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- 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/reactive-effects.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +16 -0
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +16 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +15 -0
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/module-exports.d.ts +64 -0
- package/dist/module-exports.d.ts.map +1 -1
- package/dist/scope/binding-scope.d.ts +1 -1
- package/dist/types.d.ts +112 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +117 -17
- package/src/__tests__/binding-scope-ratchet.test.ts +146 -21
- package/src/__tests__/component-type-parameters.test.ts +70 -0
- package/src/__tests__/csr-materialize-loop-preamble-shadow.test.ts +10 -1
- package/src/__tests__/doc-examples.test.ts +1 -0
- package/src/__tests__/free-refs.test.ts +1 -1
- package/src/__tests__/mutable-binding-writers.test.ts +133 -0
- package/src/__tests__/preamble-conditional-reactivity.test.ts +191 -0
- package/src/__tests__/signal-setter-updater-type.test.ts +81 -0
- package/src/adapters/jsx-adapter.ts +29 -3
- package/src/adapters/loop-bound-names.ts +18 -0
- package/src/adapters/test-adapter.ts +4 -1
- package/src/augment-inherited-props.ts +13 -1
- package/src/compiler.ts +18 -0
- package/src/debug.ts +34 -21
- package/src/free-refs.ts +14 -5
- package/src/index.ts +6 -0
- package/src/ir-to-client-js/client-only-elision.ts +11 -5
- package/src/ir-to-client-js/collect-elements.ts +17 -2
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +12 -2
- package/src/ir-to-client-js/html-template.ts +51 -0
- package/src/ir-to-client-js/reactivity.ts +4 -2
- package/src/ir-to-client-js/types.ts +16 -0
- package/src/ir-to-client-js/utils.ts +15 -0
- package/src/jsx-to-ir.ts +117 -1
- package/src/module-exports.ts +137 -0
- package/src/scope/binding-scope.ts +1 -1
- package/src/types.ts +118 -0
package/src/module-exports.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* This is a compiler-layer concern, not adapter-specific.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import ts from 'typescript'
|
|
8
9
|
import type { ComponentIR, ParamInfo } from './types.ts'
|
|
9
10
|
import { identifierPattern } from './identifier-pattern.ts'
|
|
10
11
|
|
|
@@ -161,6 +162,142 @@ export function findReachableNames(
|
|
|
161
162
|
return reachable
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Which of `candidates` does `bodyText` ASSIGN to?
|
|
167
|
+
*
|
|
168
|
+
* Reachability above answers "is this declaration referenced?", which is
|
|
169
|
+
* the right question for pruning SSR-irrelevant code. It is the wrong
|
|
170
|
+
* question for a MUTABLE binding: a surviving `let` whose only writer got
|
|
171
|
+
* pruned is left declared-and-read but never assigned, and TypeScript's
|
|
172
|
+
* control-flow analysis then narrows it to `never` at every guarded use
|
|
173
|
+
* (#2598). `closeOverWritersOfMutableBindings` uses this to restore the
|
|
174
|
+
* missing half of that pair.
|
|
175
|
+
*
|
|
176
|
+
* Recognizes the forms that actually write a local binding:
|
|
177
|
+
* `x = …`, `x += …` (and every other compound operator), `x++`, `--x`
|
|
178
|
+
* Destructuring assignment (`[x] = …`, `({ x } = …)`) is deliberately NOT
|
|
179
|
+
* recognized: it never appears in the ref/handler shapes this exists for,
|
|
180
|
+
* and a wrong guess here over-retains rather than fails loudly, so leaving
|
|
181
|
+
* it out keeps the retained set honest. If one shows up, it will present
|
|
182
|
+
* as this same `never` narrowing and can be added with a fixture.
|
|
183
|
+
*
|
|
184
|
+
* Parsed with the TS AST, not matched as text: `identifierPattern` (used
|
|
185
|
+
* for reference detection above) cannot tell a write from a read, and a
|
|
186
|
+
* regex for `name\s*=` would match `name == x`, a `name=` inside a string
|
|
187
|
+
* or JSX attribute, and a property write `obj.name = x` that assigns
|
|
188
|
+
* nothing of the sort.
|
|
189
|
+
*/
|
|
190
|
+
export function findAssignedNames(
|
|
191
|
+
bodyText: string,
|
|
192
|
+
candidates: ReadonlySet<string>,
|
|
193
|
+
): Set<string> {
|
|
194
|
+
const assigned = new Set<string>()
|
|
195
|
+
if (candidates.size === 0) return assigned
|
|
196
|
+
|
|
197
|
+
const sf = ts.createSourceFile(
|
|
198
|
+
'bf-assignment-scan.tsx',
|
|
199
|
+
bodyText,
|
|
200
|
+
ts.ScriptTarget.Latest,
|
|
201
|
+
/* setParentNodes */ false,
|
|
202
|
+
ts.ScriptKind.TSX,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
// A bare Identifier on the left of an assignment — `obj.x = …` is a
|
|
206
|
+
// PropertyAccessExpression and writes through the binding rather than to
|
|
207
|
+
// it, so it does not count.
|
|
208
|
+
const record = (target: ts.Node): void => {
|
|
209
|
+
if (ts.isIdentifier(target) && candidates.has(target.text)) {
|
|
210
|
+
assigned.add(target.text)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const visit = (node: ts.Node): void => {
|
|
215
|
+
if (ts.isBinaryExpression(node) && isAssignmentOperator(node.operatorToken.kind)) {
|
|
216
|
+
record(node.left)
|
|
217
|
+
} else if (
|
|
218
|
+
(ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
|
|
219
|
+
(node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken)
|
|
220
|
+
) {
|
|
221
|
+
record(node.operand)
|
|
222
|
+
}
|
|
223
|
+
ts.forEachChild(node, visit)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
ts.forEachChild(sf, visit)
|
|
227
|
+
return assigned
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* `findReachableNames`, plus the invariant it cannot express on its own:
|
|
232
|
+
* **a mutable binding that survives keeps the declarations that write it.**
|
|
233
|
+
*
|
|
234
|
+
* Reachability is seeded from the RENDERED JSX, which has already had the
|
|
235
|
+
* client-only attributes stripped — `ref={setRef}` leaves no `setRef`
|
|
236
|
+
* behind, and `onClick={handleClick}` is rendered as `onClick={() => {}}`.
|
|
237
|
+
* That is deliberate: code reachable only from a handler is client-only
|
|
238
|
+
* and should not be emitted into an SSR template.
|
|
239
|
+
*
|
|
240
|
+
* It goes wrong when a `let` outlives its writer. The binding survives
|
|
241
|
+
* because some OTHER surviving declaration reads it, while its only
|
|
242
|
+
* assignment lived in a pruned handler — so the emitted template declares
|
|
243
|
+
* it, reads it, and never assigns it. TypeScript's control-flow analysis
|
|
244
|
+
* concludes it is permanently `null`, narrows every guarded use to `never`,
|
|
245
|
+
* and each member access on it fails:
|
|
246
|
+
*
|
|
247
|
+
* let highlightEl: HTMLElement | null = null // writer was pruned
|
|
248
|
+
* const syncScroll = () => {
|
|
249
|
+
* if (highlightEl && textareaEl) {
|
|
250
|
+
* highlightEl.scrollTop = textareaEl.scrollTop // TS2339 on `never`
|
|
251
|
+
* }
|
|
252
|
+
* }
|
|
253
|
+
*
|
|
254
|
+
* Pulling the writers back in restores the source's shape for exactly the
|
|
255
|
+
* bindings that survived — nothing else. The retained writer is dead code
|
|
256
|
+
* at SSR (it only ever runs from a hydrated event), which is the same
|
|
257
|
+
* harmless-unused-declaration trade `generateModuleScopeDeclarations`
|
|
258
|
+
* already makes deliberately.
|
|
259
|
+
*
|
|
260
|
+
* Iterates to a fixpoint because a newly retained writer can read further
|
|
261
|
+
* declarations, and can itself write another mutable binding. Bounded by
|
|
262
|
+
* the declaration count: each round either adds a name or stops.
|
|
263
|
+
*/
|
|
264
|
+
export function closeOverWritersOfMutableBindings(
|
|
265
|
+
primaryRefs: string,
|
|
266
|
+
declarations: { name: string; body: string }[],
|
|
267
|
+
mutableNames: ReadonlySet<string>,
|
|
268
|
+
): Set<string> {
|
|
269
|
+
let reachable = findReachableNames(primaryRefs, declarations)
|
|
270
|
+
if (mutableNames.size === 0) return reachable
|
|
271
|
+
|
|
272
|
+
let seedText = primaryRefs
|
|
273
|
+
for (let round = 0; round <= declarations.length; round++) {
|
|
274
|
+
const survivingMutables = new Set(
|
|
275
|
+
[...reachable].filter(name => mutableNames.has(name)),
|
|
276
|
+
)
|
|
277
|
+
if (survivingMutables.size === 0) return reachable
|
|
278
|
+
|
|
279
|
+
const added = declarations
|
|
280
|
+
.filter(d => !reachable.has(d.name))
|
|
281
|
+
.filter(d => findAssignedNames(d.body, survivingMutables).size > 0)
|
|
282
|
+
.map(d => d.name)
|
|
283
|
+
if (added.length === 0) return reachable
|
|
284
|
+
|
|
285
|
+
// Re-seed by NAME rather than merging sets directly, so each retained
|
|
286
|
+
// writer's own transitive dependencies come along through the same
|
|
287
|
+
// traversal instead of a second, divergent one.
|
|
288
|
+
seedText += '\n' + added.join('\n')
|
|
289
|
+
reachable = findReachableNames(seedText, declarations)
|
|
290
|
+
}
|
|
291
|
+
return reachable
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function isAssignmentOperator(kind: ts.SyntaxKind): boolean {
|
|
295
|
+
return (
|
|
296
|
+
kind >= ts.SyntaxKind.FirstAssignment &&
|
|
297
|
+
kind <= ts.SyntaxKind.LastAssignment
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
164
301
|
/**
|
|
165
302
|
* Extract parameter names from a function expression string.
|
|
166
303
|
* Handles: arrow functions, single-param arrows, function expressions.
|
|
@@ -201,7 +201,7 @@ export class BindingScope {
|
|
|
201
201
|
* qualifies, including a preamble local shadowing a module const.
|
|
202
202
|
* These call `isBound` / `boundNames()`.
|
|
203
203
|
* - REACTIVITY / SLOT-ID CLASSIFIERS (`referencesLoopParam`,
|
|
204
|
-
* `hasReactiveAttributes`, and the `BindingEnvironment.
|
|
204
|
+
* `hasReactiveAttributes`, and the `BindingEnvironment.loopValueBoundNames`
|
|
205
205
|
* feed built from `makeBindingEnv`, all in `jsx-to-ir.ts`) ask
|
|
206
206
|
* "does this expression read a value that changes per row and so
|
|
207
207
|
* needs its own patchable slot" — a preamble local already gets
|
package/src/types.ts
CHANGED
|
@@ -901,6 +901,28 @@ export interface MapCallbackPreamble {
|
|
|
901
901
|
* keeps the plain interpolation it always had.
|
|
902
902
|
*/
|
|
903
903
|
builderNames: string[]
|
|
904
|
+
/**
|
|
905
|
+
* The subset of {@link declaredNames} whose OWN initializer is itself
|
|
906
|
+
* reactive — reads a signal / memo / reactive prop, directly or
|
|
907
|
+
* transitively through an earlier preamble declaration (#2596). `undefined`
|
|
908
|
+
* (never an empty array) when the preamble contributes no such name, or
|
|
909
|
+
* when it isn't a value-only declaration sequence this analysis covers
|
|
910
|
+
* (see `preambleFromValueStatements`'s caller — a JSX-building preamble
|
|
911
|
+
* doesn't compute this and leaves it unset).
|
|
912
|
+
*
|
|
913
|
+
* Used to decide whether a loop-body CONDITIONAL whose condition
|
|
914
|
+
* bare-references a preamble local should carry the IR `reactive` flag
|
|
915
|
+
* (`markPreambleConditionalReactivity`, jsx-to-ir.ts). Deliberately NOT the
|
|
916
|
+
* same test `collectPreambleRegions`/`markPreambleAttrSlots` use for
|
|
917
|
+
* text/attr positions — those mark ANY reference to `declaredNames`
|
|
918
|
+
* reactive because their region-patch effect re-runs the whole preamble
|
|
919
|
+
* unconditionally on row update, and ordinary signal auto-tracking inside
|
|
920
|
+
* that effect picks up genuine dependencies regardless of the IR flag. A
|
|
921
|
+
* conditional instead swaps whole DOM subtrees via `insert()`, so it stays
|
|
922
|
+
* unwrapped unless the local it reads is proven to depend on an actual
|
|
923
|
+
* external signal — a bare `item.title`-derived local needs no such wrap.
|
|
924
|
+
*/
|
|
925
|
+
reactiveNames?: string[]
|
|
904
926
|
}
|
|
905
927
|
|
|
906
928
|
/**
|
|
@@ -1940,6 +1962,25 @@ export interface IRMetadata {
|
|
|
1940
1962
|
isClientComponent: boolean
|
|
1941
1963
|
typeDefinitions: TypeDefinition[]
|
|
1942
1964
|
propsType: TypeInfo | null
|
|
1965
|
+
/**
|
|
1966
|
+
* The component function's own generic type parameter list, verbatim
|
|
1967
|
+
* from source (each `node.getText()`, joined and wrapped in `<...>`),
|
|
1968
|
+
* e.g. `<NodeType extends NodeBase = NodeBase, EdgeType extends
|
|
1969
|
+
* EdgeBase = EdgeBase>`. `null` when the component isn't generic.
|
|
1970
|
+
*
|
|
1971
|
+
* A generic function component's props type (and often its body) keeps
|
|
1972
|
+
* referencing these names verbatim in emitted output (e.g. `props:
|
|
1973
|
+
* FlowComponentProps<NodeType, EdgeType>`, `createFlowStore<NodeType,
|
|
1974
|
+
* EdgeType>(props)`) — without the function's own declaration also
|
|
1975
|
+
* carrying the type parameters, those references are unresolved names
|
|
1976
|
+
* in the emitted `.tsx` (TS2304). Emitters that print a `function
|
|
1977
|
+
* <name>(...)` signature for the component must splice this verbatim
|
|
1978
|
+
* between the name and the parameter list. Optional (rather than
|
|
1979
|
+
* required) so the many hand-built `IRMetadata` test fixtures across
|
|
1980
|
+
* the suite don't need updating for a field that is `null` for the
|
|
1981
|
+
* overwhelming majority of (non-generic) components.
|
|
1982
|
+
*/
|
|
1983
|
+
typeParameters?: string | null
|
|
1943
1984
|
propsParams: ParamInfo[]
|
|
1944
1985
|
/** Name of the props object parameter (e.g., 'props' in `function Component(props: Props)`) */
|
|
1945
1986
|
propsObjectName: string | null
|
|
@@ -2151,9 +2192,62 @@ export interface CompilerError {
|
|
|
2151
2192
|
suggestion?: ErrorSuggestion
|
|
2152
2193
|
}
|
|
2153
2194
|
|
|
2195
|
+
/**
|
|
2196
|
+
* The kind of escape available from a refusal — the way a user gets their
|
|
2197
|
+
* legitimate, in-subset JSX to compile on an adapter that cannot host it
|
|
2198
|
+
* (#2613).
|
|
2199
|
+
*
|
|
2200
|
+
* Lives here, next to `ErrorSuggestion`, because both halves of the
|
|
2201
|
+
* "loud-or-escapable" contract must speak the SAME enum: the diagnostic
|
|
2202
|
+
* CLAIMS kinds (`ErrorSuggestion.escape`) and a conformance fixture
|
|
2203
|
+
* DEMONSTRATES them (`JSXFixture.escapes`, which re-exports this type —
|
|
2204
|
+
* `@barefootjs/adapter-tests` depends on this package, not the reverse).
|
|
2205
|
+
* `escape-coverage.test.ts` then checks claims are a subset of what the
|
|
2206
|
+
* twins prove, which is only meaningful while the two share one union.
|
|
2207
|
+
*/
|
|
2208
|
+
export type EscapeKind = 'client-directive' | 'prop-precompute' | 'rewrite'
|
|
2209
|
+
|
|
2210
|
+
/**
|
|
2211
|
+
* What an escape costs at SSR. `'none'` — full server render, the result
|
|
2212
|
+
* is present in the server HTML. `'client-render'` — the region is EMPTY
|
|
2213
|
+
* in server HTML until hydration.
|
|
2214
|
+
*
|
|
2215
|
+
* Output-equivalence is explicitly NOT the bar for an escape (#2613): a
|
|
2216
|
+
* `/* @client *\/` region is definitionally not equivalent, and that is a
|
|
2217
|
+
* legitimate trade the user chooses. Making the cost typed and visible is
|
|
2218
|
+
* the honest substitute for pretending it doesn't exist — every renderer
|
|
2219
|
+
* that surfaces an escape surfaces its cost from THIS map, so the trade
|
|
2220
|
+
* can never be quietly dropped on the way to a user.
|
|
2221
|
+
*/
|
|
2222
|
+
export type EscapeSsrCost = 'none' | 'client-render'
|
|
2223
|
+
|
|
2224
|
+
export const ESCAPE_SSR_COST: Record<EscapeKind, EscapeSsrCost> = {
|
|
2225
|
+
// `/* @client */` — compiles and hydrates correctly, renders nothing at SSR.
|
|
2226
|
+
'client-directive': 'client-render',
|
|
2227
|
+
// The refused computation moves to an already-computed prop.
|
|
2228
|
+
'prop-precompute': 'none',
|
|
2229
|
+
// The source is restructured into an equivalent in-subset shape.
|
|
2230
|
+
rewrite: 'none',
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2154
2233
|
export interface ErrorSuggestion {
|
|
2155
2234
|
message: string
|
|
2156
2235
|
replacement?: string
|
|
2236
|
+
/**
|
|
2237
|
+
* The escape kinds this diagnostic CLAIMS are available, structured
|
|
2238
|
+
* (#2613 increment 3). Additive and one-way: `message` stays
|
|
2239
|
+
* authoritative for humans — several sites have genuinely good
|
|
2240
|
+
* site-specific prose no enum should flatten — while this field is
|
|
2241
|
+
* authoritative for machines (`bf compat`'s legend, the docs matrix,
|
|
2242
|
+
* claim verification). New and edited refusal sites populate it; older
|
|
2243
|
+
* sites migrate opportunistically, so ABSENT means "not yet declared",
|
|
2244
|
+
* never "no escape exists".
|
|
2245
|
+
*
|
|
2246
|
+
* Order carries the recommendation: list a `ssrCost: 'none'` escape
|
|
2247
|
+
* before a `'client-render'` one, matching the prose rule that a
|
|
2248
|
+
* full-SSR way out is offered first.
|
|
2249
|
+
*/
|
|
2250
|
+
escape?: ReadonlyArray<{ kind: EscapeKind }>
|
|
2157
2251
|
}
|
|
2158
2252
|
|
|
2159
2253
|
/**
|
|
@@ -2170,6 +2264,30 @@ export interface ConformancePin {
|
|
|
2170
2264
|
severity: 'error' | 'warning'
|
|
2171
2265
|
/** Tracking issue URL (known-limitation label) for this refusal, when one exists. */
|
|
2172
2266
|
issue?: string
|
|
2267
|
+
/**
|
|
2268
|
+
* Present when THIS adapter has no verified escape yet for this
|
|
2269
|
+
* refusal — the per-adapter half of #2613's "loud-or-escapable" floor
|
|
2270
|
+
* (`packages/compat/src/__tests__/escape-coverage.test.ts`). `issue` is
|
|
2271
|
+
* the tracking pointer for closing the gap (fall back to
|
|
2272
|
+
* https://github.com/piconic-ai/barefootjs/issues/2613 itself when no
|
|
2273
|
+
* more specific issue exists yet).
|
|
2274
|
+
*
|
|
2275
|
+
* Declared here, next to the refusal it qualifies, so an adapter's own
|
|
2276
|
+
* package is the sole place that states what it knows about its own
|
|
2277
|
+
* refusal — no central cross-adapter ledger to keep in sync (that was
|
|
2278
|
+
* the architectural defect increment 1 shipped with: a `packages/compat`
|
|
2279
|
+
* test hardcoding every adapter's id, which made adapters non-additive).
|
|
2280
|
+
*
|
|
2281
|
+
* Absent means the adapter believes an escape is owed here — either
|
|
2282
|
+
* already satisfied (the refused fixture's `escapes` twin compiles
|
|
2283
|
+
* clean, unpinned, non-divergent, and not CSR-skipped on THIS adapter)
|
|
2284
|
+
* or a pending gap the floor test won't let merge silently.
|
|
2285
|
+
*
|
|
2286
|
+
* Shrink-only, same discipline `KNOWN_HOLES` established: once a
|
|
2287
|
+
* working twin exists here, a lingering `unescapable` becomes a STALE
|
|
2288
|
+
* declaration and the floor test fails loudly on it, naming this pin.
|
|
2289
|
+
*/
|
|
2290
|
+
unescapable?: { issue: string }
|
|
2173
2291
|
}
|
|
2174
2292
|
/** Keyed by shared-fixture id (`JSXFixture.id`). */
|
|
2175
2293
|
export type ConformancePins = Record<string, ReadonlyArray<ConformancePin>>
|