@barefootjs/jsx 0.16.0 → 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.
Files changed (57) hide show
  1. package/dist/adapters/env-signal.d.ts +38 -15
  2. package/dist/adapters/env-signal.d.ts.map +1 -1
  3. package/dist/adapters/jsx-adapter.d.ts.map +1 -1
  4. package/dist/adapters/parsed-expr-emitter.d.ts +7 -6
  5. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  6. package/dist/analyzer-context.d.ts +29 -1
  7. package/dist/analyzer-context.d.ts.map +1 -1
  8. package/dist/analyzer.d.ts.map +1 -1
  9. package/dist/builtin-lowering-plugins.d.ts +34 -0
  10. package/dist/builtin-lowering-plugins.d.ts.map +1 -0
  11. package/dist/expression-parser.d.ts +219 -163
  12. package/dist/expression-parser.d.ts.map +1 -1
  13. package/dist/index.d.ts +7 -4
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +6754 -6129
  16. package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
  17. package/dist/ir-to-client-js/plan/build-declaration-emit.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/plan/declaration-emit.d.ts +9 -0
  19. package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
  20. package/dist/jsx-to-ir.d.ts.map +1 -1
  21. package/dist/lowering-registry.d.ts +122 -0
  22. package/dist/lowering-registry.d.ts.map +1 -0
  23. package/dist/query-href-lowering.d.ts +63 -0
  24. package/dist/query-href-lowering.d.ts.map +1 -0
  25. package/dist/ssr-defaults.d.ts.map +1 -1
  26. package/dist/types.d.ts +169 -11
  27. package/dist/types.d.ts.map +1 -1
  28. package/package.json +2 -2
  29. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +68 -3
  30. package/src/__tests__/analyzer.test.ts +53 -0
  31. package/src/__tests__/expression-parser.test.ts +703 -391
  32. package/src/__tests__/ir-reduce-op.test.ts +18 -21
  33. package/src/__tests__/ir-sort-comparator.test.ts +19 -20
  34. package/src/__tests__/lowering-registry.test.ts +141 -0
  35. package/src/__tests__/primitive-resolver-alias.test.ts +23 -0
  36. package/src/__tests__/query-href-recognition.test.ts +58 -0
  37. package/src/__tests__/serialize-parsed-expr.test.ts +204 -0
  38. package/src/__tests__/unsupported-expression.test.ts +98 -4
  39. package/src/adapters/env-signal.ts +60 -21
  40. package/src/adapters/jsx-adapter.ts +17 -0
  41. package/src/adapters/parsed-expr-emitter.ts +39 -41
  42. package/src/analyzer-context.ts +72 -27
  43. package/src/analyzer.ts +226 -9
  44. package/src/builtin-lowering-plugins.ts +54 -0
  45. package/src/expression-parser.ts +1183 -927
  46. package/src/index.ts +26 -3
  47. package/src/ir-to-client-js/csr-substitute.ts +5 -0
  48. package/src/ir-to-client-js/plan/build-declaration-emit.ts +16 -0
  49. package/src/ir-to-client-js/plan/declaration-emit.ts +9 -0
  50. package/src/ir-to-client-js/stringify/declaration-emit.ts +11 -0
  51. package/src/jsx-to-ir.ts +182 -43
  52. package/src/lowering-registry.ts +160 -0
  53. package/src/query-href-lowering.ts +147 -0
  54. package/src/ssr-defaults.ts +5 -1
  55. package/src/types.ts +171 -12
  56. package/src/__tests__/flatmap-support.test.ts +0 -218
  57. package/src/__tests__/reduce-op.test.ts +0 -201
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 hit
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) — a real user-facing
1652
- // reactive export the compiler lowers like any other `@barefootjs/client`
1653
- // signal read (SSR: a template binding; client: a `createEffect`).
1654
- 'searchParams',
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.length > 0 || importsBrowserOnlyClientApi(ctx)
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
+ }