@barefootjs/jsx 0.15.2 → 0.17.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/env-signal.d.ts +38 -15
- package/dist/adapters/env-signal.d.ts.map +1 -1
- package/dist/adapters/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +7 -6
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts +29 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/builtin-lowering-plugins.d.ts +34 -0
- package/dist/builtin-lowering-plugins.d.ts.map +1 -0
- package/dist/expression-parser.d.ts +219 -163
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6892 -6118
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/build-declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts +9 -0
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/lowering-registry.d.ts +122 -0
- package/dist/lowering-registry.d.ts.map +1 -0
- package/dist/profiler.d.ts +115 -0
- package/dist/profiler.d.ts.map +1 -1
- package/dist/query-href-lowering.d.ts +63 -0
- package/dist/query-href-lowering.d.ts.map +1 -0
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/types.d.ts +169 -11
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +68 -3
- package/src/__tests__/analyzer.test.ts +53 -0
- package/src/__tests__/expression-parser.test.ts +703 -391
- package/src/__tests__/ir-reduce-op.test.ts +18 -21
- package/src/__tests__/ir-sort-comparator.test.ts +19 -20
- package/src/__tests__/lowering-registry.test.ts +141 -0
- package/src/__tests__/primitive-resolver-alias.test.ts +23 -0
- package/src/__tests__/profiler.test.ts +149 -0
- package/src/__tests__/query-href-recognition.test.ts +58 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +204 -0
- package/src/__tests__/unsupported-expression.test.ts +98 -4
- package/src/adapters/env-signal.ts +60 -21
- package/src/adapters/jsx-adapter.ts +17 -0
- package/src/adapters/parsed-expr-emitter.ts +39 -41
- package/src/analyzer-context.ts +72 -27
- package/src/analyzer.ts +226 -9
- package/src/builtin-lowering-plugins.ts +54 -0
- package/src/expression-parser.ts +1183 -927
- package/src/index.ts +35 -3
- package/src/ir-to-client-js/csr-substitute.ts +5 -0
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +16 -0
- package/src/ir-to-client-js/plan/declaration-emit.ts +9 -0
- package/src/ir-to-client-js/stringify/declaration-emit.ts +11 -0
- package/src/jsx-to-ir.ts +182 -43
- package/src/lowering-registry.ts +160 -0
- package/src/profiler.ts +328 -0
- package/src/query-href-lowering.ts +147 -0
- package/src/ssr-defaults.ts +5 -1
- package/src/types.ts +171 -12
- package/src/__tests__/flatmap-support.test.ts +0 -218
- package/src/__tests__/reduce-op.test.ts +0 -201
package/src/analyzer-context.ts
CHANGED
|
@@ -344,11 +344,21 @@ function propertyNameText(
|
|
|
344
344
|
|
|
345
345
|
export function typeNodeToTypeInfo(
|
|
346
346
|
typeNode: ts.TypeNode | undefined,
|
|
347
|
-
sourceFile: ts.SourceFile
|
|
347
|
+
sourceFile: ts.SourceFile,
|
|
348
|
+
// When set, `raw` is derived from this extractor instead of
|
|
349
|
+
// `node.getText(sourceFile)`. Synthetic nodes (from `checker.typeToTypeNode`,
|
|
350
|
+
// used by `tsTypeToTypeInfo`) have no source positions, so `getText()` would
|
|
351
|
+
// throw — callers pass a printer-based extractor. In that mode the
|
|
352
|
+
// object-literal / function branches that need member/param source text emit
|
|
353
|
+
// a `raw`-only shape (those consumers only run on real source).
|
|
354
|
+
rawOf?: (node: ts.TypeNode) => string
|
|
348
355
|
): TypeInfo | null {
|
|
349
356
|
if (!typeNode) return null
|
|
350
357
|
|
|
351
|
-
const
|
|
358
|
+
const synthetic = rawOf !== undefined
|
|
359
|
+
const raw = rawOf ? rawOf(typeNode) : typeNode.getText(sourceFile)
|
|
360
|
+
const recurse = (n: ts.TypeNode): TypeInfo =>
|
|
361
|
+
typeNodeToTypeInfo(n, sourceFile, rawOf) ?? { kind: 'unknown', raw: 'unknown' }
|
|
352
362
|
|
|
353
363
|
// Primitive types (check by SyntaxKind)
|
|
354
364
|
switch (typeNode.kind) {
|
|
@@ -366,26 +376,12 @@ export function typeNodeToTypeInfo(
|
|
|
366
376
|
|
|
367
377
|
// Array types
|
|
368
378
|
if (ts.isArrayTypeNode(typeNode)) {
|
|
369
|
-
return {
|
|
370
|
-
kind: 'array',
|
|
371
|
-
raw,
|
|
372
|
-
elementType: typeNodeToTypeInfo(typeNode.elementType, sourceFile) ?? {
|
|
373
|
-
kind: 'unknown',
|
|
374
|
-
raw: 'unknown',
|
|
375
|
-
},
|
|
376
|
-
}
|
|
379
|
+
return { kind: 'array', raw, elementType: recurse(typeNode.elementType) }
|
|
377
380
|
}
|
|
378
381
|
|
|
379
382
|
// Union types
|
|
380
383
|
if (ts.isUnionTypeNode(typeNode)) {
|
|
381
|
-
return {
|
|
382
|
-
kind: 'union',
|
|
383
|
-
raw,
|
|
384
|
-
unionTypes: typeNode.types.map(
|
|
385
|
-
(t) =>
|
|
386
|
-
typeNodeToTypeInfo(t, sourceFile) ?? { kind: 'unknown', raw: 'unknown' }
|
|
387
|
-
),
|
|
388
|
-
}
|
|
384
|
+
return { kind: 'union', raw, unionTypes: typeNode.types.map(recurse) }
|
|
389
385
|
}
|
|
390
386
|
|
|
391
387
|
// Type literal (object type)
|
|
@@ -393,7 +389,9 @@ export function typeNodeToTypeInfo(
|
|
|
393
389
|
return {
|
|
394
390
|
kind: 'object',
|
|
395
391
|
raw,
|
|
396
|
-
|
|
392
|
+
// Synthetic members have no source positions for membersToProperties'
|
|
393
|
+
// getText(); emit a property-less object in that mode.
|
|
394
|
+
...(synthetic ? {} : { properties: membersToProperties(typeNode.members, sourceFile) }),
|
|
397
395
|
}
|
|
398
396
|
}
|
|
399
397
|
|
|
@@ -407,14 +405,7 @@ export function typeNodeToTypeInfo(
|
|
|
407
405
|
(refName === 'Array' || refName === 'ReadonlyArray') &&
|
|
408
406
|
typeNode.typeArguments?.length === 1
|
|
409
407
|
) {
|
|
410
|
-
return {
|
|
411
|
-
kind: 'array',
|
|
412
|
-
raw,
|
|
413
|
-
elementType: typeNodeToTypeInfo(typeNode.typeArguments[0], sourceFile) ?? {
|
|
414
|
-
kind: 'unknown',
|
|
415
|
-
raw: 'unknown',
|
|
416
|
-
},
|
|
417
|
-
}
|
|
408
|
+
return { kind: 'array', raw, elementType: recurse(typeNode.typeArguments[0]) }
|
|
418
409
|
}
|
|
419
410
|
return {
|
|
420
411
|
kind: 'interface',
|
|
@@ -424,6 +415,8 @@ export function typeNodeToTypeInfo(
|
|
|
424
415
|
|
|
425
416
|
// Function type
|
|
426
417
|
if (ts.isFunctionTypeNode(typeNode)) {
|
|
418
|
+
// Param names/defaults come from getText — skip them for synthetic nodes.
|
|
419
|
+
if (synthetic) return { kind: 'function', raw }
|
|
427
420
|
return {
|
|
428
421
|
kind: 'function',
|
|
429
422
|
raw,
|
|
@@ -443,6 +436,40 @@ export function typeNodeToTypeInfo(
|
|
|
443
436
|
return { kind: 'unknown', raw }
|
|
444
437
|
}
|
|
445
438
|
|
|
439
|
+
// Printer + blank source file reused across calls so checker-driven type
|
|
440
|
+
// conversion never allocates a fresh `ts.createSourceFile` per memo on the
|
|
441
|
+
// build hot path (the blank file is only printer context for synthetic
|
|
442
|
+
// nodes, created once at module load).
|
|
443
|
+
const _typePrinter = ts.createPrinter({ removeComments: true, omitTrailingSemicolon: true })
|
|
444
|
+
const _blankTypeSourceFile = ts.createSourceFile('__bf_types__.ts', '', ts.ScriptTarget.Latest)
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Convert a resolved `ts.Type` to `TypeInfo` via the type checker. Used to
|
|
448
|
+
* sharpen `createMemo` field types the syntactic `inferTypeFromValue`
|
|
449
|
+
* heuristic can't reach — e.g. `createMemo(() => generateDays())` whose body
|
|
450
|
+
* is a local-function call (→ `CalendarDay[][]`) or a ternary of typed
|
|
451
|
+
* arrays (→ `string[]`). Without this the Go adapter renders such memos as
|
|
452
|
+
* `map[string]interface{}` / `bool` placeholders, so a typed backend can't
|
|
453
|
+
* populate the SSR data (#1968). Returns `null` when the type can't be
|
|
454
|
+
* lowered to a `ts.TypeNode`.
|
|
455
|
+
*
|
|
456
|
+
* Shares the structural lowering in `typeNodeToTypeInfo` (via the `rawOf`
|
|
457
|
+
* extractor) so node→TypeInfo logic lives in one place; the synthetic nodes
|
|
458
|
+
* from `typeToTypeNode` have no source text, so `raw` comes from the printer.
|
|
459
|
+
*/
|
|
460
|
+
export function tsTypeToTypeInfo(type: ts.Type, checker: ts.TypeChecker): TypeInfo | null {
|
|
461
|
+
const node = checker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.NoTruncation)
|
|
462
|
+
if (!node) return null
|
|
463
|
+
const rawOf = (n: ts.TypeNode): string => {
|
|
464
|
+
try {
|
|
465
|
+
return _typePrinter.printNode(ts.EmitHint.Unspecified, n, _blankTypeSourceFile)
|
|
466
|
+
} catch {
|
|
467
|
+
return 'unknown'
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return typeNodeToTypeInfo(node, _blankTypeSourceFile, rawOf)
|
|
471
|
+
}
|
|
472
|
+
|
|
446
473
|
// =============================================================================
|
|
447
474
|
// AST Helpers
|
|
448
475
|
// =============================================================================
|
|
@@ -474,3 +501,21 @@ export function isArrowComponentFunction(
|
|
|
474
501
|
if (!node.initializer || !ts.isArrowFunction(node.initializer)) return false
|
|
475
502
|
return true
|
|
476
503
|
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The set of reactive getter names for a component — signal accessors plus memo
|
|
507
|
+
* names. Single source of truth for "what counts as a reactive getter", shared
|
|
508
|
+
* by the analyzer's block-memo fold purity oracle (#2040) and `jsx-to-ir`'s
|
|
509
|
+
* `getReactiveGetterNames`. A reactive read is idempotent within a render, so
|
|
510
|
+
* callers treat `getter()` as pure. If a third reactive kind is added, update
|
|
511
|
+
* this one function.
|
|
512
|
+
*/
|
|
513
|
+
export function collectReactiveGetterNames(
|
|
514
|
+
signals: ReadonlyArray<{ getter: string }>,
|
|
515
|
+
memos: ReadonlyArray<{ name: string }>,
|
|
516
|
+
): Set<string> {
|
|
517
|
+
const names = new Set<string>()
|
|
518
|
+
for (const s of signals) names.add(s.getter)
|
|
519
|
+
for (const m of memos) names.add(m.name)
|
|
520
|
+
return names
|
|
521
|
+
}
|
package/src/analyzer.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import ts from 'typescript'
|
|
10
10
|
import type { ImportSpecifier, TypeInfo, ParamInfo, ReactiveFactoryInfo } from './types.ts'
|
|
11
|
+
import { parseExpression, parseBlockBodyTolerant, foldBlockToExpr } from './expression-parser.ts'
|
|
11
12
|
import { rewriteBarePropRefs } from './prop-rewrite.ts'
|
|
12
13
|
import { incrementCounter } from './instrumentation.ts'
|
|
13
14
|
import {
|
|
@@ -16,9 +17,11 @@ import {
|
|
|
16
17
|
createAnalyzerContext,
|
|
17
18
|
getSourceLocation,
|
|
18
19
|
typeNodeToTypeInfo,
|
|
20
|
+
tsTypeToTypeInfo,
|
|
19
21
|
membersToProperties,
|
|
20
22
|
isComponentFunction,
|
|
21
23
|
isArrowComponentFunction,
|
|
24
|
+
collectReactiveGetterNames,
|
|
22
25
|
} from './analyzer-context.ts'
|
|
23
26
|
import { createError, createWarning, ErrorCodes } from './errors.ts'
|
|
24
27
|
import path from 'node:path'
|
|
@@ -260,6 +263,37 @@ export function analyzeComponent(
|
|
|
260
263
|
// Post-processing validations
|
|
261
264
|
validateContext(ctx)
|
|
262
265
|
|
|
266
|
+
// Roadmap A: carry a best-effort structured parse of each signal's initial
|
|
267
|
+
// value so adapters lower a literal init (`useState(['a', 'b'])`) from the
|
|
268
|
+
// tree instead of re-parsing the string with `ts.createSourceFile`. The value
|
|
269
|
+
// is parenthesised before parsing so a bare object-literal init
|
|
270
|
+
// (`createSignal({ a: 1 })`) resolves to an `object-literal` rather than being
|
|
271
|
+
// read as a block statement; `parseExpression` unwraps the parens, so arrays /
|
|
272
|
+
// scalars / prop refs are unchanged. An unsupported shape leaves `parsed`
|
|
273
|
+
// undefined and the adapter falls back. Runs after `scanImportedClientSignals`
|
|
274
|
+
// so imported signals are covered too.
|
|
275
|
+
for (const signal of ctx.signals) {
|
|
276
|
+
if (!signal.initialValue) continue
|
|
277
|
+
const parsed = parseExpression(`(${signal.initialValue})`)
|
|
278
|
+
if (parsed.kind !== 'unsupported') signal.parsed = parsed
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// #2040: fold a complete, value-producing block-bodied memo into a single
|
|
282
|
+
// expression so it flows through the same `parsed` path as an expression-bodied
|
|
283
|
+
// memo, instead of relying on per-idiom block recognizers (#1897 / #1945 /
|
|
284
|
+
// #2015). Runs after all signals/memos are collected so the reactive-getter
|
|
285
|
+
// set (used as the purity oracle — an idempotent signal/memo read may be
|
|
286
|
+
// inlined on several branches) is complete. An incomplete block (the tolerant
|
|
287
|
+
// parser dropped a statement it couldn't represent) or one that doesn't fold
|
|
288
|
+
// (imperative residue) leaves `parsed` undefined and consumers keep their
|
|
289
|
+
// existing `parsedBlock` fallback.
|
|
290
|
+
const reactiveGetterNames = collectReactiveGetterNames(ctx.signals, ctx.memos)
|
|
291
|
+
for (const memo of ctx.memos) {
|
|
292
|
+
if (memo.parsed || !memo.parsedBlock || !memo.parsedBlockComplete) continue
|
|
293
|
+
const folded = foldBlockToExpr(memo.parsedBlock, { pureCallNames: reactiveGetterNames })
|
|
294
|
+
if (folded.ok) memo.parsed = folded.expr
|
|
295
|
+
}
|
|
296
|
+
|
|
263
297
|
return ctx
|
|
264
298
|
}
|
|
265
299
|
|
|
@@ -960,6 +994,24 @@ const PRIMITIVE_CANONICAL_NAMES: Record<string, 'signal' | 'memo' | 'effect' | '
|
|
|
960
994
|
createEffect: 'effect',
|
|
961
995
|
onMount: 'onMount',
|
|
962
996
|
onCleanup: 'onCleanup',
|
|
997
|
+
// Request-scoped env-signal factories (#2057) are `createSignal`-shaped, so
|
|
998
|
+
// they resolve to the `signal` kind and flow through the normal signal
|
|
999
|
+
// collection + fold purity oracle. Their env key (see ENV_SIGNAL_FACTORIES)
|
|
1000
|
+
// is recorded separately on the collected signal so adapters can lower the
|
|
1001
|
+
// reader value; the reactivity kind itself is just `signal`.
|
|
1002
|
+
createSearchParams: 'signal',
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Env-signal factories → the request-env key their getter reads
|
|
1007
|
+
* (`createSearchParams` → `'search'`, matching the runtime's
|
|
1008
|
+
* `createEnvSignal('search', …)`). Recognising the *factory* is a general
|
|
1009
|
+
* mechanism (like the reactive-primitive names above); the resulting signal is
|
|
1010
|
+
* tagged with the key so adapters lower its reader value from structure, with
|
|
1011
|
+
* no `searchParams`-name allow-list (#2057, superseding #2055).
|
|
1012
|
+
*/
|
|
1013
|
+
const ENV_SIGNAL_FACTORIES: Record<string, string> = {
|
|
1014
|
+
createSearchParams: 'search',
|
|
963
1015
|
}
|
|
964
1016
|
|
|
965
1017
|
type PrimitiveKind = (typeof PRIMITIVE_CANONICAL_NAMES)[keyof typeof PRIMITIVE_CANONICAL_NAMES]
|
|
@@ -1020,6 +1072,22 @@ function resolveCalleeViaChecker(
|
|
|
1020
1072
|
ident: ts.Identifier,
|
|
1021
1073
|
ctx: AnalyzerContext
|
|
1022
1074
|
): PrimitiveKind | null {
|
|
1075
|
+
const name = resolveCanonicalClientExportName(ident, ctx)
|
|
1076
|
+
return name ? (PRIMITIVE_CANONICAL_NAMES[name] ?? null) : null
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Resolve an identifier back to the canonical export name it was imported under
|
|
1081
|
+
* from `@barefootjs/client`, following alias chains
|
|
1082
|
+
* (`import { createSignal as sig }`). Returns null when the checker is
|
|
1083
|
+
* unavailable, the symbol doesn't resolve, or its declaration doesn't live in
|
|
1084
|
+
* `@barefootjs/client` — so a user-defined function that happens to share a
|
|
1085
|
+
* name never matches. Shared by the primitive-kind and env-signal resolvers.
|
|
1086
|
+
*/
|
|
1087
|
+
function resolveCanonicalClientExportName(
|
|
1088
|
+
ident: ts.Identifier,
|
|
1089
|
+
ctx: AnalyzerContext
|
|
1090
|
+
): string | null {
|
|
1023
1091
|
if (!ctx.checker) return null
|
|
1024
1092
|
let symbol: ts.Symbol | undefined
|
|
1025
1093
|
try {
|
|
@@ -1039,20 +1107,41 @@ function resolveCalleeViaChecker(
|
|
|
1039
1107
|
return null
|
|
1040
1108
|
}
|
|
1041
1109
|
}
|
|
1042
|
-
const originalName = target.getName()
|
|
1043
|
-
const hit = PRIMITIVE_CANONICAL_NAMES[originalName]
|
|
1044
|
-
if (!hit) return null
|
|
1045
1110
|
// Confirm the declaration actually lives in @barefootjs/client so we
|
|
1046
1111
|
// don't match a user-defined function that happens to share the name.
|
|
1047
1112
|
for (const decl of target.declarations ?? []) {
|
|
1048
1113
|
const sourceName = decl.getSourceFile().fileName
|
|
1049
1114
|
if (sourceName.includes('@barefootjs/client') || sourceName.includes('packages/client/')) {
|
|
1050
|
-
return
|
|
1115
|
+
return target.getName()
|
|
1051
1116
|
}
|
|
1052
1117
|
}
|
|
1053
1118
|
return null
|
|
1054
1119
|
}
|
|
1055
1120
|
|
|
1121
|
+
/**
|
|
1122
|
+
* Resolve a call expression to the request-env key of the env-signal factory it
|
|
1123
|
+
* calls (`createSearchParams()` → `'search'`), or null if it isn't one. Mirrors
|
|
1124
|
+
* {@link resolvePrimitiveKind}'s fast/slow paths (direct name, alias via
|
|
1125
|
+
* checker, `bf.createSearchParams` namespace access) against
|
|
1126
|
+
* {@link ENV_SIGNAL_FACTORIES}.
|
|
1127
|
+
*/
|
|
1128
|
+
function resolveEnvSignalKey(
|
|
1129
|
+
callExpr: ts.CallExpression,
|
|
1130
|
+
ctx: AnalyzerContext
|
|
1131
|
+
): string | null {
|
|
1132
|
+
if (ts.isIdentifier(callExpr.expression)) {
|
|
1133
|
+
const key = ENV_SIGNAL_FACTORIES[callExpr.expression.text]
|
|
1134
|
+
if (key) return key
|
|
1135
|
+
const canonical = resolveCanonicalClientExportName(callExpr.expression, ctx)
|
|
1136
|
+
return canonical ? (ENV_SIGNAL_FACTORIES[canonical] ?? null) : null
|
|
1137
|
+
}
|
|
1138
|
+
if (ts.isPropertyAccessExpression(callExpr.expression)) {
|
|
1139
|
+
const key = ENV_SIGNAL_FACTORIES[callExpr.expression.name.text]
|
|
1140
|
+
if (key && isBarefootClientNamespace(callExpr.expression.expression, ctx)) return key
|
|
1141
|
+
}
|
|
1142
|
+
return null
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1056
1145
|
/**
|
|
1057
1146
|
* Check whether an expression refers to a namespace import of
|
|
1058
1147
|
* `@barefootjs/client`, e.g. `import * as bf from '@barefootjs/client'` →
|
|
@@ -1127,6 +1216,11 @@ function collectSignal(node: ts.VariableDeclaration, ctx: AnalyzerContext): void
|
|
|
1127
1216
|
type = inferTypeFromValue(initialValue)
|
|
1128
1217
|
}
|
|
1129
1218
|
|
|
1219
|
+
const envReader = resolveEnvSignalKey(callExpr, ctx) ?? undefined
|
|
1220
|
+
// The factory as written (identifier, alias, or `ns.factory`), so emit re-emits
|
|
1221
|
+
// the binding actually in scope rather than a hardcoded canonical name (#2057).
|
|
1222
|
+
const envFactory = envReader ? callExpr.expression.getText(ctx.sourceFile) : undefined
|
|
1223
|
+
|
|
1130
1224
|
ctx.signals.push({
|
|
1131
1225
|
getter,
|
|
1132
1226
|
setter,
|
|
@@ -1137,6 +1231,8 @@ function collectSignal(node: ts.VariableDeclaration, ctx: AnalyzerContext): void
|
|
|
1137
1231
|
initialFreeIdentifiers: callExpr.arguments[0]
|
|
1138
1232
|
? extractFreeIdentifiersFromNode(callExpr.arguments[0])
|
|
1139
1233
|
: new Set(),
|
|
1234
|
+
envReader,
|
|
1235
|
+
envFactory,
|
|
1140
1236
|
})
|
|
1141
1237
|
}
|
|
1142
1238
|
|
|
@@ -1334,6 +1430,28 @@ function isMemoDeclaration(node: ts.VariableDeclaration, ctx: AnalyzerContext):
|
|
|
1334
1430
|
return resolvePrimitiveKind(node.initializer, ctx) === 'memo'
|
|
1335
1431
|
}
|
|
1336
1432
|
|
|
1433
|
+
/**
|
|
1434
|
+
* Does the memo arrow's effective body resolve to a template literal? Mirrors
|
|
1435
|
+
* the Go adapter's former `isTemplateLiteralMemo` (which re-parsed `computation`
|
|
1436
|
+
* with `ts.createSourceFile`) but runs on the real arrow node at analysis time:
|
|
1437
|
+
* unwrap parens, descend a block body to its first `return`, and check for a
|
|
1438
|
+
* template expression / no-substitution template literal.
|
|
1439
|
+
*/
|
|
1440
|
+
function memoBodyIsTemplateLiteral(memoArrow: ts.Expression | undefined): boolean {
|
|
1441
|
+
let node: ts.Node | undefined = memoArrow
|
|
1442
|
+
while (node && ts.isParenthesizedExpression(node)) node = node.expression
|
|
1443
|
+
if (!node || !ts.isArrowFunction(node)) return false
|
|
1444
|
+
let body: ts.Node = node.body
|
|
1445
|
+
while (ts.isParenthesizedExpression(body)) body = body.expression
|
|
1446
|
+
if (ts.isBlock(body)) {
|
|
1447
|
+
const ret = body.statements.find(ts.isReturnStatement)
|
|
1448
|
+
if (!ret || !ret.expression) return false
|
|
1449
|
+
body = ret.expression
|
|
1450
|
+
while (ts.isParenthesizedExpression(body)) body = body.expression
|
|
1451
|
+
}
|
|
1452
|
+
return ts.isTemplateExpression(body) || ts.isNoSubstitutionTemplateLiteral(body)
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1337
1455
|
function collectMemo(node: ts.VariableDeclaration, ctx: AnalyzerContext): void {
|
|
1338
1456
|
const name = (node.name as ts.Identifier).text
|
|
1339
1457
|
const callExpr = node.initializer as ts.CallExpression
|
|
@@ -1363,10 +1481,76 @@ function collectMemo(node: ts.VariableDeclaration, ctx: AnalyzerContext): void {
|
|
|
1363
1481
|
}
|
|
1364
1482
|
}
|
|
1365
1483
|
|
|
1484
|
+
// When the syntactic heuristic above can't resolve a precise type
|
|
1485
|
+
// (`object`/`unknown` — e.g. a local-function call or a ternary of typed
|
|
1486
|
+
// arrays), ask the type checker for the memo body's actual type. This is
|
|
1487
|
+
// what lets the Go adapter emit `[][]CalendarDay` / `[]string` / `bool`
|
|
1488
|
+
// instead of `map[string]interface{}` / `bool` placeholders (#1968), so a
|
|
1489
|
+
// typed backend can populate the SSR data. Only upgrades imprecise results —
|
|
1490
|
+
// already-precise syntactic types are left untouched.
|
|
1491
|
+
if (
|
|
1492
|
+
ctx.checker &&
|
|
1493
|
+
(type.kind === 'unknown' || type.kind === 'object') &&
|
|
1494
|
+
callExpr.arguments[0] &&
|
|
1495
|
+
(ts.isArrowFunction(callExpr.arguments[0]) || ts.isFunctionExpression(callExpr.arguments[0]))
|
|
1496
|
+
) {
|
|
1497
|
+
const fnType = ctx.checker.getTypeAtLocation(callExpr.arguments[0])
|
|
1498
|
+
const sig = fnType.getCallSignatures()[0]
|
|
1499
|
+
if (sig) {
|
|
1500
|
+
const inferred = tsTypeToTypeInfo(ctx.checker.getReturnTypeOfSignature(sig), ctx.checker)
|
|
1501
|
+
if (inferred && inferred.kind !== 'unknown') type = inferred
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// Structured parse of the arrow BODY, so adapters can shape-match the memo
|
|
1506
|
+
// on a tree instead of re-parsing `computation`. Parse from the type-STRIPPED
|
|
1507
|
+
// body (`ctx.getJS`, same source as `computation`) — `getText` would keep
|
|
1508
|
+
// TypeScript-only syntax (`as T`, `!`, `satisfies`) that `parseExpression`
|
|
1509
|
+
// rejects, leaving `parsed` undefined for typed bodies that the stripped
|
|
1510
|
+
// `computation` would match. Expression-bodied arrows only — block bodies
|
|
1511
|
+
// (`() => { … }`) and unsupported shapes leave `parsed` undefined and
|
|
1512
|
+
// consumers fall back to `computation`.
|
|
1513
|
+
const memoArrow = callExpr.arguments[0]
|
|
1514
|
+
const parsedBody =
|
|
1515
|
+
memoArrow && ts.isArrowFunction(memoArrow) && !ts.isBlock(memoArrow.body)
|
|
1516
|
+
? parseExpression(ctx.getJS(memoArrow.body))
|
|
1517
|
+
: undefined
|
|
1518
|
+
// `object-literal` is excluded alongside `unsupported`: an object-returning
|
|
1519
|
+
// memo (`() => ({ … })`) isn't lowered from the parsed tree yet, so leaving
|
|
1520
|
+
// `parsed` undefined keeps the adapter on its existing object-memo lowering
|
|
1521
|
+
// (byte-identical; flipped in a later Roadmap A unit).
|
|
1522
|
+
const parsed =
|
|
1523
|
+
parsedBody && parsedBody.kind !== 'unsupported' && parsedBody.kind !== 'object-literal'
|
|
1524
|
+
? parsedBody
|
|
1525
|
+
: undefined
|
|
1526
|
+
|
|
1527
|
+
// Block-bodied memos: carry the statements (tolerant — unparseable ones are
|
|
1528
|
+
// omitted) so adapters can pattern-match block shapes (e.g. a guard-and-
|
|
1529
|
+
// return-const memo) without re-parsing `computation`. Unwrap parens around
|
|
1530
|
+
// the arrow to match the former adapter walks.
|
|
1531
|
+
let arrowNode: ts.Node | undefined = memoArrow
|
|
1532
|
+
while (arrowNode && ts.isParenthesizedExpression(arrowNode)) arrowNode = arrowNode.expression
|
|
1533
|
+
const blockBody =
|
|
1534
|
+
arrowNode && ts.isArrowFunction(arrowNode) && ts.isBlock(arrowNode.body)
|
|
1535
|
+
? arrowNode.body
|
|
1536
|
+
: undefined
|
|
1537
|
+
const parsedBlock = blockBody
|
|
1538
|
+
? parseBlockBodyTolerant(blockBody, ctx.sourceFile, node => ctx.getJS(node))
|
|
1539
|
+
: undefined
|
|
1540
|
+
// `parseBlockBodyTolerant` runs `parseStatement` once per source statement and
|
|
1541
|
+
// pushes only the non-null results, so equal lengths mean every statement was
|
|
1542
|
+
// represented — nothing silently omitted (see `MemoInfo.parsedBlockComplete`).
|
|
1543
|
+
const parsedBlockComplete =
|
|
1544
|
+
parsedBlock && blockBody ? parsedBlock.length === blockBody.statements.length : undefined
|
|
1545
|
+
|
|
1366
1546
|
ctx.memos.push({
|
|
1367
1547
|
name,
|
|
1368
1548
|
computation,
|
|
1549
|
+
parsedBlock,
|
|
1550
|
+
parsedBlockComplete,
|
|
1369
1551
|
typedComputation: typedComputation !== computation ? typedComputation : undefined,
|
|
1552
|
+
parsed,
|
|
1553
|
+
bodyIsTemplateLiteral: memoBodyIsTemplateLiteral(memoArrow),
|
|
1370
1554
|
type,
|
|
1371
1555
|
deps,
|
|
1372
1556
|
loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
|
|
@@ -1648,10 +1832,15 @@ const CLIENT_EXPORTS = new Set([
|
|
|
1648
1832
|
'forwardProps', 'unwrap', '__slot',
|
|
1649
1833
|
'createContext', 'useContext', 'provideContext',
|
|
1650
1834
|
'createPortal', 'isSSRPortal', 'findSiblingSlot', 'cleanupPortalPlaceholder',
|
|
1651
|
-
// Request-scoped environment signal (router v0.5) —
|
|
1652
|
-
//
|
|
1653
|
-
//
|
|
1654
|
-
|
|
1835
|
+
// Request-scoped environment signal factory (router v0.5) — `createSignal`-
|
|
1836
|
+
// shaped, recognised structurally (#2057) so its getter is just a signal
|
|
1837
|
+
// getter; the compiler lowers the reader value per adapter via the signal's
|
|
1838
|
+
// `envReader` key, with no `searchParams`-name allow-list.
|
|
1839
|
+
'createSearchParams',
|
|
1840
|
+
// Pure URL-query builder (#2042) — the functional counterpart to
|
|
1841
|
+
// `searchParams`. Runs natively on the client; SSR adapters lower a
|
|
1842
|
+
// `queryHref(base, { … })` call to their query helper (go-template: `bf_query`).
|
|
1843
|
+
'queryHref',
|
|
1655
1844
|
// Compile-away JSX built-ins (#1915) — importing them is what scopes the
|
|
1656
1845
|
// compiler's `<Async>` / `<Region>` recognition; the import is elided on emit.
|
|
1657
1846
|
'Async', 'Region',
|
|
@@ -2713,9 +2902,28 @@ function collectConstant(
|
|
|
2713
2902
|
}
|
|
2714
2903
|
}
|
|
2715
2904
|
|
|
2905
|
+
// Structure a module-scope constant's value for adapters (Roadmap A). Only
|
|
2906
|
+
// module consts are carried — they're the ones adapters resolve as
|
|
2907
|
+
// compile-time records (e.g. a `strokePaths` icon map). Parse the
|
|
2908
|
+
// PARENTHESISED value so a bare object literal (`{ … }`), which TS reads as
|
|
2909
|
+
// a block at statement position, resolves to an `object-literal` instead of
|
|
2910
|
+
// failing. Best-effort and inert for inlined JSX (no usable value tree).
|
|
2911
|
+
//
|
|
2912
|
+
// Carried for component-scope consts too (#2018 P5): the Go constructor
|
|
2913
|
+
// lowerers (`lowerCtorExpr`, helper inlining) read this single generic tree —
|
|
2914
|
+
// which now models the multi-param-arrow and regex shapes the former
|
|
2915
|
+
// Go-only `parsed2` carried — to inline a derived component const's value
|
|
2916
|
+
// (`base || '/'`) recursively. Best-effort; an unrepresentable shape leaves
|
|
2917
|
+
// `parsed` undefined and consumers fall back to the string.
|
|
2918
|
+
const parsed =
|
|
2919
|
+
value && !isJsx && !isJsxFunction
|
|
2920
|
+
? parseExpression(`(${value.trim()})`)
|
|
2921
|
+
: undefined
|
|
2922
|
+
|
|
2716
2923
|
ctx.localConstants.push({
|
|
2717
2924
|
name,
|
|
2718
2925
|
value,
|
|
2926
|
+
parsed,
|
|
2719
2927
|
typedValue: typedValue !== value ? typedValue : undefined,
|
|
2720
2928
|
valueBranches,
|
|
2721
2929
|
declarationKind,
|
|
@@ -3097,8 +3305,11 @@ function validateContext(ctx: AnalyzerContext): void {
|
|
|
3097
3305
|
// their implementations live in `@barefootjs/client/runtime` and require
|
|
3098
3306
|
// compiler emission to run correctly.
|
|
3099
3307
|
if (!ctx.hasUseClientDirective) {
|
|
3308
|
+
// Env signals (`createSearchParams()`, #2057) are exempt: reading the
|
|
3309
|
+
// request query is SSR-safe and hydrates without `use client`, exactly as
|
|
3310
|
+
// the pre-#2057 bare `searchParams()` import did (it was not a signal).
|
|
3100
3311
|
const usesBrowserOnlyApi =
|
|
3101
|
-
ctx.signals.
|
|
3312
|
+
ctx.signals.some(s => !s.envReader) || importsBrowserOnlyClientApi(ctx)
|
|
3102
3313
|
if (usesBrowserOnlyApi) {
|
|
3103
3314
|
ctx.errors.push(
|
|
3104
3315
|
createError(ErrorCodes.MISSING_USE_CLIENT, {
|
|
@@ -3827,6 +4038,12 @@ export function validateReactiveFactoryCalls(ctx: AnalyzerContext): void {
|
|
|
3827
4038
|
if (!ts.isIdentifier(decl.initializer.expression)) continue
|
|
3828
4039
|
const callee = decl.initializer.expression.text
|
|
3829
4040
|
if (callee === 'createSignal' || callee === 'createMemo') continue
|
|
4041
|
+
// Env-signal factories (`createSearchParams`, #2057) are `createSignal`-
|
|
4042
|
+
// shaped and recognised structurally — a valid tuple destructure. Resolve
|
|
4043
|
+
// via the same path as recognition (`resolveEnvSignalKey`) so an aliased
|
|
4044
|
+
// import (`import { createSearchParams as csp }`) is accepted here too,
|
|
4045
|
+
// rather than falling through to a spurious BF110.
|
|
4046
|
+
if (resolveEnvSignalKey(decl.initializer, ctx)) continue
|
|
3830
4047
|
// Inlined factories were rewritten away before this analysis, so
|
|
3831
4048
|
// anything still matching the shape is a destructure of an
|
|
3832
4049
|
// unrecognised callee (imported helper, ad-hoc tuple fn, factory
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in lowering plugins — shipped with the compiler and applied by default,
|
|
3
|
+
* with no `barefoot.config.ts` registration required (#2057).
|
|
4
|
+
*
|
|
5
|
+
* These use the *exact same* {@link LoweringPlugin} seam as userland plugins.
|
|
6
|
+
* "Built-in" means only that the compiler registers them itself, so consumers
|
|
7
|
+
* get them for free. This is deliberate: it keeps a first-party API like
|
|
8
|
+
* `queryHref` from being a bespoke special-case branch in every adapter. Instead
|
|
9
|
+
* of each adapter carrying an `if (isQueryHref) …` recognizer, `queryHref` is a
|
|
10
|
+
* pre-registered plugin — indistinguishable, at the adapter, from any other. The
|
|
11
|
+
* adapters have one path (registry matcher → neutral node → render), and the
|
|
12
|
+
* only queryHref-specific knowledge left is the plugin registration below.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { LoweringPlugin } from './lowering-registry.ts'
|
|
16
|
+
import { registerLoweringPlugin } from './lowering-registry.ts'
|
|
17
|
+
import { queryHrefLocalNames } from './adapters/env-signal.ts'
|
|
18
|
+
import { matchQueryHrefCall } from './query-href-lowering.ts'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `queryHref(base, { … })` — the pure URL-query builder (#2042). Its runtime
|
|
22
|
+
* lives in `@barefootjs/client`; this plugin recognises the call structurally
|
|
23
|
+
* and returns a backend-neutral `guard-list` on the `query` helper, which each
|
|
24
|
+
* adapter maps to its own runtime helper (`bf_query` / `bf->query` / `$bf.query`).
|
|
25
|
+
* `prepare` resolves the local names `queryHref` is imported under once per
|
|
26
|
+
* component; a component that never imports it gets no matcher (the adapter skips
|
|
27
|
+
* it entirely).
|
|
28
|
+
*/
|
|
29
|
+
export const queryHrefPlugin: LoweringPlugin = {
|
|
30
|
+
name: 'queryHref',
|
|
31
|
+
prepare(metadata) {
|
|
32
|
+
const locals = queryHrefLocalNames(metadata)
|
|
33
|
+
if (locals.size === 0) return null
|
|
34
|
+
return (callee, args) => {
|
|
35
|
+
const q = matchQueryHrefCall(callee, args, locals)
|
|
36
|
+
return q
|
|
37
|
+
? { kind: 'guard-list', helper: 'query', base: q.base, triples: q.triples }
|
|
38
|
+
: null
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Every plugin the compiler ships and applies by default. */
|
|
44
|
+
export const BUILTIN_LOWERING_PLUGINS: readonly LoweringPlugin[] = [queryHrefPlugin]
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Register the built-in plugins into the shared registry. Called for its side
|
|
48
|
+
* effect when `@barefootjs/jsx` is loaded (see `index.ts`), so adapters see
|
|
49
|
+
* `queryHref` without any explicit setup. Idempotent — `registerLoweringPlugin`
|
|
50
|
+
* dedups by name, so re-invoking (e.g. after a test reset) can't stack copies.
|
|
51
|
+
*/
|
|
52
|
+
export function registerBuiltinLoweringPlugins(): void {
|
|
53
|
+
for (const plugin of BUILTIN_LOWERING_PLUGINS) registerLoweringPlugin(plugin)
|
|
54
|
+
}
|