@barefootjs/jsx 0.14.0 → 0.15.1
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 +40 -0
- package/dist/adapters/env-signal.d.ts.map +1 -0
- package/dist/adapters/parsed-expr-emitter.d.ts +2 -1
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/augment-inherited-props.d.ts +42 -1
- package/dist/augment-inherited-props.d.ts.map +1 -1
- package/dist/builtins.d.ts +33 -0
- package/dist/builtins.d.ts.map +1 -0
- package/dist/compiler.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +48 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +392 -26
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/compiler-stress-1244.test.ts +4 -2
- package/src/__tests__/expression-parser.test.ts +92 -1
- package/src/__tests__/ir-async.test.ts +8 -0
- package/src/__tests__/ir-builtin-import-scope.test.ts +188 -0
- package/src/__tests__/ir-region.test.ts +86 -0
- package/src/__tests__/ssr-defaults.test.ts +25 -0
- package/src/adapters/env-signal.ts +75 -0
- package/src/adapters/parsed-expr-emitter.ts +11 -0
- package/src/analyzer.ts +9 -0
- package/src/augment-inherited-props.ts +170 -2
- package/src/builtins.ts +63 -0
- package/src/compiler.ts +6 -2
- package/src/errors.ts +10 -0
- package/src/expression-parser.ts +156 -2
- package/src/index.ts +8 -2
- package/src/ir-to-client-js/imports.ts +5 -0
- package/src/jsx-to-ir.ts +189 -8
- package/src/ssr-defaults.ts +55 -17
- package/src/types.ts +16 -0
package/src/expression-parser.ts
CHANGED
|
@@ -17,6 +17,14 @@ export type ParsedExpr =
|
|
|
17
17
|
| { kind: 'literal'; value: string | number | boolean | null; literalType: 'string' | 'number' | 'boolean' | 'null' }
|
|
18
18
|
| { kind: 'call'; callee: ParsedExpr; args: ParsedExpr[] }
|
|
19
19
|
| { kind: 'member'; object: ParsedExpr; property: string; computed: boolean }
|
|
20
|
+
// Element access with a NON-literal index (`selected()[index]`,
|
|
21
|
+
// `rows[i + 1]`). A literal-index access (`arr[0]`, `obj['key']`)
|
|
22
|
+
// stays a `member` (computed) since the key is statically known and
|
|
23
|
+
// folds into the same property path. The variable case can't, so the
|
|
24
|
+
// index travels as its own `ParsedExpr` for the adapter to lower
|
|
25
|
+
// (array `->[$i]` vs hash `->{$k}` in Perl, `[index]` in JS). #1897
|
|
26
|
+
// (data-table's per-row `selected()[index]`).
|
|
27
|
+
| { kind: 'index-access'; object: ParsedExpr; index: ParsedExpr }
|
|
20
28
|
| { kind: 'binary'; op: string; left: ParsedExpr; right: ParsedExpr }
|
|
21
29
|
| { kind: 'unary'; op: string; argument: ParsedExpr }
|
|
22
30
|
| { kind: 'conditional'; test: ParsedExpr; consequent: ParsedExpr; alternate: ParsedExpr }
|
|
@@ -46,6 +54,7 @@ export type ParsedExpr =
|
|
|
46
54
|
| 'toLowerCase'
|
|
47
55
|
| 'toUpperCase'
|
|
48
56
|
| 'trim'
|
|
57
|
+
| 'toFixed'
|
|
49
58
|
| 'split'
|
|
50
59
|
| 'startsWith'
|
|
51
60
|
| 'endsWith'
|
|
@@ -476,6 +485,106 @@ export function extractArrowBodyExpression(source: string): string | null {
|
|
|
476
485
|
return expr.body.getText(sf).trim()
|
|
477
486
|
}
|
|
478
487
|
|
|
488
|
+
/**
|
|
489
|
+
* One member of a context-provider object-literal value
|
|
490
|
+
* (`<Ctx.Provider value={{ open: () => …, onOpenChange: … }}>`), classified
|
|
491
|
+
* for SSR lowering:
|
|
492
|
+
*
|
|
493
|
+
* - `getter` — a ZERO-parameter arrow with an expression body. At SSR time
|
|
494
|
+
* the provider value is fixed for the render, so the getter is equivalent
|
|
495
|
+
* to its body's value snapshot (`open: () => props.open ?? false` reads as
|
|
496
|
+
* `props.open ?? false`). Arrows with parameters are NOT getters — their
|
|
497
|
+
* body references the parameter, which has no SSR value.
|
|
498
|
+
* - `function` — any other function shape (parameterised / block-bodied
|
|
499
|
+
* arrow, function expression, or a `??` / `||` chain with a function
|
|
500
|
+
* operand). These are behavior, not data: SSR never invokes them, so
|
|
501
|
+
* adapters lower them to their nil value (`undef` / `nil`).
|
|
502
|
+
* - `expression` — everything else; lowers through the adapter's normal
|
|
503
|
+
* expression pipeline (so signal getters, props, memo calls keep their
|
|
504
|
+
* existing SSR seeding semantics).
|
|
505
|
+
*/
|
|
506
|
+
export type ProviderObjectMember =
|
|
507
|
+
| { name: string; kind: 'getter'; body: string }
|
|
508
|
+
| { name: string; kind: 'function' }
|
|
509
|
+
| { name: string; kind: 'expression'; expr: string }
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Structurally parse a `<Ctx.Provider value={{ … }}>` object literal into
|
|
513
|
+
* per-member SSR lowering classifications (see `ProviderObjectMember`).
|
|
514
|
+
*
|
|
515
|
+
* Returns `null` when the source is not a plain object literal, or when it
|
|
516
|
+
* contains a shape with no per-member story (spread entry, computed key,
|
|
517
|
+
* get/set accessor) — callers fall back to their existing whole-expression
|
|
518
|
+
* path (typically a BF101 refusal). Shorthand members (`{ search }`) yield
|
|
519
|
+
* an `expression` member with the identifier as the expression; method
|
|
520
|
+
* members (`{ open() {…} }`) classify as `function` like block-bodied
|
|
521
|
+
* arrows.
|
|
522
|
+
*/
|
|
523
|
+
export function parseProviderObjectLiteral(source: string): ProviderObjectMember[] | null {
|
|
524
|
+
const sf = ts.createSourceFile(
|
|
525
|
+
'__provider__.ts',
|
|
526
|
+
`const __x = (${source});`,
|
|
527
|
+
ts.ScriptTarget.Latest,
|
|
528
|
+
/* setParentNodes */ true,
|
|
529
|
+
)
|
|
530
|
+
const stmt = sf.statements[0]
|
|
531
|
+
if (!stmt || !ts.isVariableStatement(stmt)) return null
|
|
532
|
+
let init = stmt.declarationList.declarations[0]?.initializer
|
|
533
|
+
while (init && ts.isParenthesizedExpression(init)) init = init.expression
|
|
534
|
+
if (!init || !ts.isObjectLiteralExpression(init)) return null
|
|
535
|
+
|
|
536
|
+
const isFunctionShaped = (e: ts.Expression): boolean => {
|
|
537
|
+
let v: ts.Expression = e
|
|
538
|
+
while (ts.isParenthesizedExpression(v)) v = v.expression
|
|
539
|
+
if (ts.isArrowFunction(v) || ts.isFunctionExpression(v)) return true
|
|
540
|
+
// `props.onX ?? (() => {})` — a fallback chain with a function operand
|
|
541
|
+
// is function-typed regardless of which side wins at runtime.
|
|
542
|
+
if (
|
|
543
|
+
ts.isBinaryExpression(v) &&
|
|
544
|
+
(v.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ||
|
|
545
|
+
v.operatorToken.kind === ts.SyntaxKind.BarBarToken)
|
|
546
|
+
) {
|
|
547
|
+
return isFunctionShaped(v.left) || isFunctionShaped(v.right)
|
|
548
|
+
}
|
|
549
|
+
return false
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const members: ProviderObjectMember[] = []
|
|
553
|
+
for (const prop of init.properties) {
|
|
554
|
+
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
555
|
+
members.push({ name: prop.name.text, kind: 'expression', expr: prop.name.text })
|
|
556
|
+
continue
|
|
557
|
+
}
|
|
558
|
+
if (ts.isMethodDeclaration(prop)) {
|
|
559
|
+
// `{ open() {…} }` — function-shaped behavior, same as a
|
|
560
|
+
// block-bodied arrow member.
|
|
561
|
+
const name = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)
|
|
562
|
+
? prop.name.text
|
|
563
|
+
: null
|
|
564
|
+
if (name === null) return null // computed key
|
|
565
|
+
members.push({ name, kind: 'function' })
|
|
566
|
+
continue
|
|
567
|
+
}
|
|
568
|
+
if (!ts.isPropertyAssignment(prop)) return null // spread / accessor
|
|
569
|
+
const name = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)
|
|
570
|
+
? prop.name.text
|
|
571
|
+
: null
|
|
572
|
+
if (name === null) return null // computed key
|
|
573
|
+
let v: ts.Expression = prop.initializer
|
|
574
|
+
while (ts.isParenthesizedExpression(v)) v = v.expression
|
|
575
|
+
if (ts.isArrowFunction(v) && v.parameters.length === 0 && !ts.isBlock(v.body)) {
|
|
576
|
+
members.push({ name, kind: 'getter', body: v.body.getText(sf).trim() })
|
|
577
|
+
continue
|
|
578
|
+
}
|
|
579
|
+
if (isFunctionShaped(v)) {
|
|
580
|
+
members.push({ name, kind: 'function' })
|
|
581
|
+
continue
|
|
582
|
+
}
|
|
583
|
+
members.push({ name, kind: 'expression', expr: v.getText(sf).trim() })
|
|
584
|
+
}
|
|
585
|
+
return members
|
|
586
|
+
}
|
|
587
|
+
|
|
479
588
|
/**
|
|
480
589
|
* A single entry of a JSX `style={{ … }}` object, lowered for SSR. The key is
|
|
481
590
|
* already CSS-cased (`backgroundColor` → `background-color`); the value is
|
|
@@ -757,6 +866,14 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
757
866
|
if (callee.property === 'trim') {
|
|
758
867
|
return { kind: 'array-method', method: 'trim', object: callee.object, args }
|
|
759
868
|
}
|
|
869
|
+
// `.toFixed(digits?)` — Number → fixed-decimal string. The digit
|
|
870
|
+
// count (default 0) travels as the single arg; all adapters route
|
|
871
|
+
// through a `to_fixed` runtime helper (Perl) / `fmt.Sprintf` (Go)
|
|
872
|
+
// so JS's rounding + zero-padding semantics match. #1897
|
|
873
|
+
// (data-table's `payment.amount.toFixed(2)`).
|
|
874
|
+
if (callee.property === 'toFixed') {
|
|
875
|
+
return { kind: 'array-method', method: 'toFixed', object: callee.object, args }
|
|
876
|
+
}
|
|
760
877
|
// `.split()` / `.split(sep)` / `.split(sep, limit)` — string →
|
|
761
878
|
// array, full JS arity. `.split()` (no separator) returns the
|
|
762
879
|
// whole string as a single element; `.split(sep)` splits on the
|
|
@@ -1028,6 +1145,13 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
1028
1145
|
if (ts.isElementAccessExpression(node)) {
|
|
1029
1146
|
const object = convertNode(node.expression, raw)
|
|
1030
1147
|
const argNode = node.argumentExpression
|
|
1148
|
+
// `argumentExpression` is non-optional in the TS types but CAN be
|
|
1149
|
+
// undefined on an AST recovered from incomplete source (`arr[`). Guard
|
|
1150
|
+
// so a half-typed expression surfaces a recoverable BF101 instead of
|
|
1151
|
+
// throwing inside `ts.isNumericLiteral(undefined)`.
|
|
1152
|
+
if (!argNode) {
|
|
1153
|
+
return { kind: 'unsupported', raw, reason: 'Element access with no index expression' }
|
|
1154
|
+
}
|
|
1031
1155
|
// For simple number/string access, store as property
|
|
1032
1156
|
if (ts.isNumericLiteral(argNode)) {
|
|
1033
1157
|
return { kind: 'member', object, property: argNode.text, computed: true }
|
|
@@ -1035,8 +1159,13 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
1035
1159
|
if (ts.isStringLiteral(argNode)) {
|
|
1036
1160
|
return { kind: 'member', object, property: argNode.text, computed: true }
|
|
1037
1161
|
}
|
|
1038
|
-
//
|
|
1039
|
-
|
|
1162
|
+
// Variable / expression index (`selected()[index]`, `rows[i + 1]`):
|
|
1163
|
+
// carry the index as its own ParsedExpr so the adapter can lower it
|
|
1164
|
+
// (the literal forms above fold into a static property path; this
|
|
1165
|
+
// one can't). #1897 (data-table).
|
|
1166
|
+
const index = convertNode(argNode, raw)
|
|
1167
|
+
if (index.kind === 'unsupported') return index
|
|
1168
|
+
return { kind: 'index-access', object, index }
|
|
1040
1169
|
}
|
|
1041
1170
|
|
|
1042
1171
|
// Binary expression: a === b, count > 0, a + b
|
|
@@ -1988,6 +2117,8 @@ function findImpureDefaultNode(expr: ParsedExpr): string | null {
|
|
|
1988
2117
|
return null
|
|
1989
2118
|
case 'member':
|
|
1990
2119
|
return findImpureDefaultNode(expr.object)
|
|
2120
|
+
case 'index-access':
|
|
2121
|
+
return findImpureDefaultNode(expr.object) ?? findImpureDefaultNode(expr.index)
|
|
1991
2122
|
case 'unary':
|
|
1992
2123
|
return findImpureDefaultNode(expr.argument)
|
|
1993
2124
|
case 'binary':
|
|
@@ -2217,6 +2348,10 @@ function collectIdentifiers(expr: ParsedExpr, out: Set<string>): void {
|
|
|
2217
2348
|
case 'member':
|
|
2218
2349
|
collectIdentifiers(expr.object, out)
|
|
2219
2350
|
return
|
|
2351
|
+
case 'index-access':
|
|
2352
|
+
collectIdentifiers(expr.object, out)
|
|
2353
|
+
collectIdentifiers(expr.index, out)
|
|
2354
|
+
return
|
|
2220
2355
|
case 'binary':
|
|
2221
2356
|
case 'logical':
|
|
2222
2357
|
collectIdentifiers(expr.left, out)
|
|
@@ -2311,6 +2446,8 @@ function substituteDestructuredFields(
|
|
|
2311
2446
|
}
|
|
2312
2447
|
}
|
|
2313
2448
|
return { kind: 'member', object: walk(e.object), property: e.property, computed: e.computed }
|
|
2449
|
+
case 'index-access':
|
|
2450
|
+
return { kind: 'index-access', object: walk(e.object), index: walk(e.index) }
|
|
2314
2451
|
case 'binary':
|
|
2315
2452
|
return { kind: 'binary', op: e.op, left: walk(e.left), right: walk(e.right) }
|
|
2316
2453
|
case 'logical':
|
|
@@ -2559,6 +2696,17 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2559
2696
|
return { supported: true, level: 'L2' }
|
|
2560
2697
|
}
|
|
2561
2698
|
|
|
2699
|
+
case 'index-access': {
|
|
2700
|
+
// `arr[index]` — supported when both the receiver and the index
|
|
2701
|
+
// expression are themselves supported (the index is typically a
|
|
2702
|
+
// loop variable or arithmetic over one). #1897 (data-table).
|
|
2703
|
+
const objSupport = checkSupport(expr.object)
|
|
2704
|
+
if (!objSupport.supported) return objSupport
|
|
2705
|
+
const indexSupport = checkSupport(expr.index)
|
|
2706
|
+
if (!indexSupport.supported) return indexSupport
|
|
2707
|
+
return { supported: true, level: 'L2' }
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2562
2710
|
case 'binary': {
|
|
2563
2711
|
const leftSupport = checkSupport(expr.left)
|
|
2564
2712
|
if (!leftSupport.supported) return leftSupport
|
|
@@ -2640,6 +2788,8 @@ export function containsHigherOrder(expr: ParsedExpr): boolean {
|
|
|
2640
2788
|
return expr.args.some(containsHigherOrder) || containsHigherOrder(expr.callee)
|
|
2641
2789
|
case 'member':
|
|
2642
2790
|
return containsHigherOrder(expr.object)
|
|
2791
|
+
case 'index-access':
|
|
2792
|
+
return containsHigherOrder(expr.object) || containsHigherOrder(expr.index)
|
|
2643
2793
|
case 'binary':
|
|
2644
2794
|
return containsHigherOrder(expr.left) || containsHigherOrder(expr.right)
|
|
2645
2795
|
case 'unary':
|
|
@@ -2811,6 +2961,8 @@ export function exprToString(expr: ParsedExpr): string {
|
|
|
2811
2961
|
return `${exprToString(expr.callee)}(${expr.args.map(exprToString).join(', ')})`
|
|
2812
2962
|
case 'member':
|
|
2813
2963
|
return `${exprToString(expr.object)}.${expr.property}`
|
|
2964
|
+
case 'index-access':
|
|
2965
|
+
return `${exprToString(expr.object)}[${exprToString(expr.index)}]`
|
|
2814
2966
|
case 'binary':
|
|
2815
2967
|
return `${exprToString(expr.left)} ${expr.op} ${exprToString(expr.right)}`
|
|
2816
2968
|
case 'unary':
|
|
@@ -2892,6 +3044,8 @@ export function stringifyParsedExpr(expr: ParsedExpr): string {
|
|
|
2892
3044
|
: JSON.stringify(expr.property)
|
|
2893
3045
|
return `${obj}[${key}]`
|
|
2894
3046
|
}
|
|
3047
|
+
case 'index-access':
|
|
3048
|
+
return `${stringifyParsedExpr(expr.object)}[${stringifyParsedExpr(expr.index)}]`
|
|
2895
3049
|
case 'binary':
|
|
2896
3050
|
return `${stringifyParsedExpr(expr.left)} ${expr.op} ${stringifyParsedExpr(expr.right)}`
|
|
2897
3051
|
case 'unary':
|
package/src/index.ts
CHANGED
|
@@ -76,6 +76,7 @@ export type { JsxAdapterConfig } from './adapters/jsx-adapter.ts'
|
|
|
76
76
|
export { rewriteImportsForTemplate } from './adapters/template-imports.ts'
|
|
77
77
|
export { emitParsedExpr } from './adapters/parsed-expr-emitter.ts'
|
|
78
78
|
export type { ParsedExprEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
|
|
79
|
+
export { importsSearchParams, searchParamsLocalNames, matchSearchParamsMethodCall } from './adapters/env-signal.ts'
|
|
79
80
|
export { emitIRNode } from './adapters/ir-node-emitter.ts'
|
|
80
81
|
export type { IRNodeEmitter, EmitIRNode } from './adapters/ir-node-emitter.ts'
|
|
81
82
|
export { emitAttrValue } from './adapters/attr-value-emitter.ts'
|
|
@@ -149,6 +150,11 @@ export type ExternalSpec =
|
|
|
149
150
|
/**
|
|
150
151
|
* An entry point to bundle directly with esbuild.
|
|
151
152
|
* Externals declared in `BuildOptions.externals` are applied automatically.
|
|
153
|
+
* `@barefootjs/client`, `@barefootjs/client/runtime`, and
|
|
154
|
+
* `@barefootjs/client/reactive` are always kept external, so you never need
|
|
155
|
+
* to list them here. They resolve through the page's import map to the shared
|
|
156
|
+
* `barefoot.js` runtime; inlining them would fork the reactive runtime and
|
|
157
|
+
* duplicate signals (#927).
|
|
152
158
|
* Use this for modules that are not barefoot components (e.g. plain TS entry
|
|
153
159
|
* points that import external vendor packages).
|
|
154
160
|
*/
|
|
@@ -247,7 +253,7 @@ export {
|
|
|
247
253
|
export { ErrorCodes, createError, formatError, generateCodeFrame } from './errors.ts'
|
|
248
254
|
|
|
249
255
|
// Expression Parser
|
|
250
|
-
export { parseExpression, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries } from './expression-parser.ts'
|
|
256
|
+
export { parseExpression, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries, parseProviderObjectLiteral, type ProviderObjectMember } from './expression-parser.ts'
|
|
251
257
|
export type { StyleObjectEntry } from './expression-parser.ts'
|
|
252
258
|
export type { ParsedExpr, ParsedStatement, SortComparator, SortKey, ReduceOp, FlatDepth, FlatMapOp, FlatMapLeaf, SupportLevel, SupportResult, TemplatePart } from './expression-parser.ts'
|
|
253
259
|
export { buildLoopChainExpr } from './loop-chain.ts'
|
|
@@ -354,7 +360,7 @@ export type {
|
|
|
354
360
|
export { BOOLEAN_ATTRS, isBooleanAttr } from './html-constants.ts'
|
|
355
361
|
|
|
356
362
|
// Shared props-object-pattern helpers for the Go / Mojo template adapters
|
|
357
|
-
export { augmentInheritedPropAccesses, parseRecordIndexAccess, evalStringArrayJoin, collectContextConsumers } from './augment-inherited-props.ts'
|
|
363
|
+
export { augmentInheritedPropAccesses, parseRecordIndexAccess, evalStringArrayJoin, collectModuleStringConsts, lookupStaticRecordLiteral, collectContextConsumers } from './augment-inherited-props.ts'
|
|
358
364
|
export type { RecordIndexAccess, RecordIndexEntry, ContextConsumer } from './augment-inherited-props.ts'
|
|
359
365
|
|
|
360
366
|
// HTML element attribute types
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ComponentIR, IRNode } from '../types.ts'
|
|
6
|
+
import { isClientBuiltinName } from '../builtins.ts'
|
|
6
7
|
|
|
7
8
|
// All exports from @barefootjs/client/runtime that may be used in generated code
|
|
8
9
|
export const RUNTIME_IMPORT_CANDIDATES = [
|
|
@@ -62,6 +63,10 @@ export function collectUserDomImports(ir: ComponentIR): string[] {
|
|
|
62
63
|
if (runtimeSources.has(imp.source) && !imp.isTypeOnly) {
|
|
63
64
|
for (const spec of imp.specifiers) {
|
|
64
65
|
if (!spec.isDefault && !spec.isNamespace) {
|
|
66
|
+
// Compile-away built-ins (`<Async>` / `<Region>`) are lowered into
|
|
67
|
+
// the template — never emit their import into the client bundle,
|
|
68
|
+
// where it would be a phantom runtime import (#1915).
|
|
69
|
+
if (isClientBuiltinName(spec.name)) continue
|
|
65
70
|
userImports.push(spec.alias ? `${spec.name} as ${spec.alias}` : spec.name)
|
|
66
71
|
}
|
|
67
72
|
}
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -35,12 +35,14 @@ import {
|
|
|
35
35
|
import { type AnalyzerContext, type MultiReturnJsxInfo, getSourceLocation } from './analyzer-context.ts'
|
|
36
36
|
import { parseExpression, isSupported, parseBlockBody, extractSortComparatorFromTS, cssKebabCase, type ParsedExpr, type ParsedStatement, type SortComparator } from './expression-parser.ts'
|
|
37
37
|
import { createError, ErrorCodes, internalInvariant } from './errors.ts'
|
|
38
|
+
import { CLIENT_BUILTIN_SOURCE, isClientBuiltinName, type ClientBuiltinTag } from './builtins.ts'
|
|
38
39
|
import { containsReactiveExpression } from './reactivity-checker.ts'
|
|
39
40
|
import {
|
|
40
41
|
rewriteBarePropRefs as rewriteBarePropRefsCore,
|
|
41
42
|
collectAstPropRefs,
|
|
42
43
|
} from './prop-rewrite.ts'
|
|
43
44
|
import { resolveFreeRefs, type BindingEnvironment } from './free-refs.ts'
|
|
45
|
+
import { computeFileScope } from './ir-to-client-js/component-scope.ts'
|
|
44
46
|
import { extractFreeIdentifiersFromNode, initializerShapeContainsJsx } from './analyzer.ts'
|
|
45
47
|
import { iterateJsTokens, replaceInExprContexts } from './scanner/js-scanner.ts'
|
|
46
48
|
|
|
@@ -78,6 +80,8 @@ interface TransformContext {
|
|
|
78
80
|
loopParams: Set<string>
|
|
79
81
|
/** Counter for async boundary IDs (a0, a1, ...) */
|
|
80
82
|
asyncIdCounter: number
|
|
83
|
+
/** Counter for <Region> structural index (0, 1, ...) within a file. */
|
|
84
|
+
regionIdCounter: number
|
|
81
85
|
/** Counter for loop marker IDs (l0, l1, ...) — separate from slot IDs so element bf="sN" numbering stays stable across versions (#1087). */
|
|
82
86
|
loopMarkerCounter: number
|
|
83
87
|
/**
|
|
@@ -137,6 +141,14 @@ interface TransformContext {
|
|
|
137
141
|
* See #1425.
|
|
138
142
|
*/
|
|
139
143
|
_branchScopePropDeps?: Map<string, Set<string>>
|
|
144
|
+
/**
|
|
145
|
+
* Lazily computed map of local JSX tag name → compile-away built-in
|
|
146
|
+
* (`Async` / `Region`), derived from `@barefootjs/client` imports (#1915).
|
|
147
|
+
* Recognition is import-scoped (not a bare tag-name match) so a user's own
|
|
148
|
+
* `<Async>` / `<Region>` component doesn't collide with the built-in, and an
|
|
149
|
+
* aliased `import { Async as Boundary }` maps `<Boundary>` to the built-in.
|
|
150
|
+
*/
|
|
151
|
+
_clientBuiltinTags?: Map<string, ClientBuiltinTag>
|
|
140
152
|
}
|
|
141
153
|
|
|
142
154
|
/**
|
|
@@ -366,6 +378,7 @@ function createTransformContext(analyzer: AnalyzerContext): TransformContext {
|
|
|
366
378
|
filePath: analyzer.filePath,
|
|
367
379
|
slotIdCounter: 0,
|
|
368
380
|
asyncIdCounter: 0,
|
|
381
|
+
regionIdCounter: 0,
|
|
369
382
|
loopMarkerCounter: 0,
|
|
370
383
|
spreadIdCounter: 0,
|
|
371
384
|
isRoot: true,
|
|
@@ -705,6 +718,101 @@ function transformNode(node: ts.Node, ctx: TransformContext): IRNode | null {
|
|
|
705
718
|
// JSX Element Transformation
|
|
706
719
|
// =============================================================================
|
|
707
720
|
|
|
721
|
+
/**
|
|
722
|
+
* Map a local JSX tag name to its compile-away built-in (`Async` / `Region`)
|
|
723
|
+
* if it was imported from `@barefootjs/client` (#1915). Recognition is
|
|
724
|
+
* import-scoped — keyed off `imports` metadata, never a bare tag-name match —
|
|
725
|
+
* so a user's own `<Async>` / `<Region>` component does not collide with the
|
|
726
|
+
* built-in, and `import { Async as Boundary }` maps `<Boundary>` to it.
|
|
727
|
+
* Memoized on `ctx`; the import list is fixed for the compile.
|
|
728
|
+
*/
|
|
729
|
+
function clientBuiltinTags(ctx: TransformContext): Map<string, ClientBuiltinTag> {
|
|
730
|
+
if (ctx._clientBuiltinTags) return ctx._clientBuiltinTags
|
|
731
|
+
const map = new Map<string, ClientBuiltinTag>()
|
|
732
|
+
for (const imp of ctx.analyzer.imports) {
|
|
733
|
+
// Require a *value* import: the tag is used as a JSX value, and the design
|
|
734
|
+
// is import-value-required. `import type { Async }` brings no value binding
|
|
735
|
+
// into scope (and is never a runtime import), so it does not scope the
|
|
736
|
+
// built-in — `<Async>` then falls through to BF054 (#1915 review).
|
|
737
|
+
if (imp.source !== CLIENT_BUILTIN_SOURCE || imp.isTypeOnly) continue
|
|
738
|
+
for (const spec of imp.specifiers) {
|
|
739
|
+
// Skip per-specifier `import { type Async }` — no value binding.
|
|
740
|
+
if (spec.isDefault || spec.isNamespace || spec.isTypeOnly) continue
|
|
741
|
+
if (isClientBuiltinName(spec.name)) {
|
|
742
|
+
map.set(spec.alias ?? spec.name, spec.name)
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
ctx._clientBuiltinTags = map
|
|
747
|
+
return map
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Whether `name` resolves to any in-scope value binding — an import (by its
|
|
752
|
+
* local name), a local function / constant, or an ambient `declare`. Used to
|
|
753
|
+
* keep the BF054 "import the built-in" diagnostic from firing when the author
|
|
754
|
+
* legitimately has their own `<Async>` / `<Region>` binding.
|
|
755
|
+
*/
|
|
756
|
+
function isNameBound(ctx: TransformContext, name: string): boolean {
|
|
757
|
+
const a = ctx.analyzer
|
|
758
|
+
if (a.ambientGlobals.has(name)) return true
|
|
759
|
+
if (a.localFunctions.some(f => f.name === name)) return true
|
|
760
|
+
if (a.localConstants.some(c => c.name === name)) return true
|
|
761
|
+
for (const imp of a.imports) {
|
|
762
|
+
// Type-only imports create a type binding, not a value one — they can't
|
|
763
|
+
// back a JSX value tag, so they must not suppress BF054. Applies to both
|
|
764
|
+
// `import type { ... }` and per-specifier `import { type X }` (#1915 review).
|
|
765
|
+
if (imp.isTypeOnly) continue
|
|
766
|
+
for (const spec of imp.specifiers) {
|
|
767
|
+
if (spec.isTypeOnly) continue
|
|
768
|
+
if ((spec.alias ?? spec.name) === name) return true
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return false
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function reportBuiltinNotImported(
|
|
775
|
+
ctx: TransformContext,
|
|
776
|
+
node: ts.Node,
|
|
777
|
+
tagName: ClientBuiltinTag,
|
|
778
|
+
): void {
|
|
779
|
+
ctx.analyzer.errors.push(
|
|
780
|
+
createError(
|
|
781
|
+
ErrorCodes.BUILTIN_REQUIRES_IMPORT,
|
|
782
|
+
getSourceLocation(node, ctx.sourceFile, ctx.filePath),
|
|
783
|
+
{
|
|
784
|
+
severity: 'error',
|
|
785
|
+
message: `<${tagName}> must be imported from '${CLIENT_BUILTIN_SOURCE}' to be recognised as a compiler built-in.`,
|
|
786
|
+
suggestion: {
|
|
787
|
+
message: `Add: import { ${tagName} } from '${CLIENT_BUILTIN_SOURCE}'`,
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
),
|
|
791
|
+
)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Dispatch a built-in JSX tag (`Async` / `Region`) when import-scoped
|
|
796
|
+
* recognition matches, or emit BF054 when the bare built-in name is used
|
|
797
|
+
* without the import and without any other in-scope binding. Returns the
|
|
798
|
+
* lowered IR node, or `null` to fall through to normal component handling.
|
|
799
|
+
*/
|
|
800
|
+
function dispatchClientBuiltin(
|
|
801
|
+
tagName: string,
|
|
802
|
+
ctx: TransformContext,
|
|
803
|
+
diagNode: ts.Node,
|
|
804
|
+
transformAsync: () => IRNode,
|
|
805
|
+
transformRegion: () => IRNode,
|
|
806
|
+
): IRNode | null {
|
|
807
|
+
const builtin = clientBuiltinTags(ctx).get(tagName)
|
|
808
|
+
if (builtin === 'Async') return transformAsync()
|
|
809
|
+
if (builtin === 'Region') return transformRegion()
|
|
810
|
+
if (isClientBuiltinName(tagName) && !isNameBound(ctx, tagName)) {
|
|
811
|
+
reportBuiltinNotImported(ctx, diagNode, tagName)
|
|
812
|
+
}
|
|
813
|
+
return null
|
|
814
|
+
}
|
|
815
|
+
|
|
708
816
|
function transformJsxElement(
|
|
709
817
|
node: ts.JsxElement,
|
|
710
818
|
ctx: TransformContext
|
|
@@ -716,10 +824,16 @@ function transformJsxElement(
|
|
|
716
824
|
return transformProviderElement(node, ctx, tagName)
|
|
717
825
|
}
|
|
718
826
|
|
|
719
|
-
// Detect Async
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
827
|
+
// Detect compile-away built-ins (`<Async>` / `<Region>`), recognised by
|
|
828
|
+
// their `@barefootjs/client` import rather than by tag name (#1915).
|
|
829
|
+
const builtin = dispatchClientBuiltin(
|
|
830
|
+
tagName,
|
|
831
|
+
ctx,
|
|
832
|
+
node.openingElement,
|
|
833
|
+
() => transformAsyncElement(node, ctx),
|
|
834
|
+
() => transformRegionElement(node, ctx),
|
|
835
|
+
)
|
|
836
|
+
if (builtin) return builtin
|
|
723
837
|
|
|
724
838
|
const isComponent = /^[A-Z]/.test(tagName)
|
|
725
839
|
|
|
@@ -782,10 +896,16 @@ function transformSelfClosingElement(
|
|
|
782
896
|
return transformSelfClosingProviderElement(node, ctx, tagName)
|
|
783
897
|
}
|
|
784
898
|
|
|
785
|
-
// Detect Async
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
899
|
+
// Detect compile-away built-ins (`<Async />` / `<Region />`), recognised by
|
|
900
|
+
// their `@barefootjs/client` import rather than by tag name (#1915).
|
|
901
|
+
const builtin = dispatchClientBuiltin(
|
|
902
|
+
tagName,
|
|
903
|
+
ctx,
|
|
904
|
+
node,
|
|
905
|
+
() => transformSelfClosingAsyncElement(node, ctx),
|
|
906
|
+
() => transformSelfClosingRegionElement(node, ctx),
|
|
907
|
+
)
|
|
908
|
+
if (builtin) return builtin
|
|
789
909
|
|
|
790
910
|
const isComponent = /^[A-Z]/.test(tagName)
|
|
791
911
|
|
|
@@ -1007,6 +1127,67 @@ function transformSelfClosingAsyncElement(
|
|
|
1007
1127
|
}
|
|
1008
1128
|
}
|
|
1009
1129
|
|
|
1130
|
+
/**
|
|
1131
|
+
* Lower `<Region>{children}</Region>` to a plain wrapper element carrying the
|
|
1132
|
+
* `bf-region` marker (spec/router.md "Regions"). The id is deterministic —
|
|
1133
|
+
* `<file scope>:<index>` — so a layout that compiles to one shared partial
|
|
1134
|
+
* emits the *same* id across every page that composes it, which is what the
|
|
1135
|
+
* client router matches on. `<Region>` is recognised by its `@barefootjs/client`
|
|
1136
|
+
* import (import-scoped, not a bare tag-name match — #1915).
|
|
1137
|
+
*/
|
|
1138
|
+
function regionId(ctx: TransformContext): string {
|
|
1139
|
+
return `${computeFileScope(ctx.filePath)}:${ctx.regionIdCounter++}`
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function transformRegionElement(
|
|
1143
|
+
node: ts.JsxElement,
|
|
1144
|
+
ctx: TransformContext
|
|
1145
|
+
): IRElement {
|
|
1146
|
+
const id = regionId(ctx)
|
|
1147
|
+
|
|
1148
|
+
// Mirror transformHtmlElement's isRoot bookkeeping so the region's children
|
|
1149
|
+
// are not mistaken for component roots.
|
|
1150
|
+
const needsScope = ctx.isRoot
|
|
1151
|
+
ctx.isRoot = false
|
|
1152
|
+
|
|
1153
|
+
const children = transformChildren(node.children, ctx)
|
|
1154
|
+
|
|
1155
|
+
return {
|
|
1156
|
+
type: 'element',
|
|
1157
|
+
tag: 'div',
|
|
1158
|
+
attrs: [],
|
|
1159
|
+
events: [],
|
|
1160
|
+
ref: null,
|
|
1161
|
+
children,
|
|
1162
|
+
slotId: null,
|
|
1163
|
+
needsScope,
|
|
1164
|
+
regionId: id,
|
|
1165
|
+
loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function transformSelfClosingRegionElement(
|
|
1170
|
+
node: ts.JsxSelfClosingElement,
|
|
1171
|
+
ctx: TransformContext
|
|
1172
|
+
): IRElement {
|
|
1173
|
+
const id = regionId(ctx)
|
|
1174
|
+
const needsScope = ctx.isRoot
|
|
1175
|
+
ctx.isRoot = false
|
|
1176
|
+
|
|
1177
|
+
return {
|
|
1178
|
+
type: 'element',
|
|
1179
|
+
tag: 'div',
|
|
1180
|
+
attrs: [],
|
|
1181
|
+
events: [],
|
|
1182
|
+
ref: null,
|
|
1183
|
+
children: [],
|
|
1184
|
+
slotId: null,
|
|
1185
|
+
needsScope,
|
|
1186
|
+
regionId: id,
|
|
1187
|
+
loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1010
1191
|
// =============================================================================
|
|
1011
1192
|
// Component Transformation
|
|
1012
1193
|
// =============================================================================
|
package/src/ssr-defaults.ts
CHANGED
|
@@ -65,6 +65,12 @@ export interface SsrDefault {
|
|
|
65
65
|
const UNRESOLVED = Symbol('unresolved')
|
|
66
66
|
type EvalResult = unknown | typeof UNRESOLVED
|
|
67
67
|
|
|
68
|
+
// Sentinel: a statement list ran to its end without hitting a `return`
|
|
69
|
+
// (so the enclosing branch falls through to the next statement). Kept
|
|
70
|
+
// distinct from `UNRESOLVED` (couldn't evaluate) so a falsy guard
|
|
71
|
+
// (`if (!key) return X`) continues evaluation instead of bailing.
|
|
72
|
+
const NO_RETURN = Symbol('no-return')
|
|
73
|
+
|
|
68
74
|
interface EvalContext {
|
|
69
75
|
/** Identifier name → previously-resolved value (signal getters, memos). */
|
|
70
76
|
bindings: Record<string, EvalResult>
|
|
@@ -230,6 +236,53 @@ function tryStaticEval(expr: string, ctx: EvalContext): EvalResult {
|
|
|
230
236
|
return evalNode(node, ctx)
|
|
231
237
|
}
|
|
232
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Evaluate a block-body's statements for its returned value. Handles
|
|
241
|
+
* `const` declarations (bound into `ctx.bindings`, which mutate in
|
|
242
|
+
* place so later statements and nested branches see them), `return`
|
|
243
|
+
* statements, and `if (cond) …` guards whose condition is statically
|
|
244
|
+
* resolvable — the early-return-on-default-state shape of
|
|
245
|
+
* an `@client`-annotated memo (`const key = sortKey(); if (!key)
|
|
246
|
+
* return payments; … sort …`). A resolvable-but-falsy guard continues
|
|
247
|
+
* to the next statement (`NO_RETURN` from the skipped branch); an
|
|
248
|
+
* unresolvable condition or any other statement kind bails to
|
|
249
|
+
* `UNRESOLVED`. #1897 (data-table's `sortedData`).
|
|
250
|
+
*/
|
|
251
|
+
function evalStatementsForReturn(
|
|
252
|
+
statements: readonly ts.Statement[],
|
|
253
|
+
ctx: EvalContext,
|
|
254
|
+
): EvalResult | typeof NO_RETURN {
|
|
255
|
+
for (const stmt of statements) {
|
|
256
|
+
if (ts.isVariableStatement(stmt)) {
|
|
257
|
+
for (const d of stmt.declarationList.declarations) {
|
|
258
|
+
if (!ts.isIdentifier(d.name) || !d.initializer) continue
|
|
259
|
+
const v = evalNode(d.initializer, ctx)
|
|
260
|
+
// Leave unresolved locals unbound; only a `return` / guard
|
|
261
|
+
// referencing one would then surface UNRESOLVED.
|
|
262
|
+
if (v !== UNRESOLVED) ctx.bindings[d.name.text] = v
|
|
263
|
+
}
|
|
264
|
+
} else if (ts.isReturnStatement(stmt)) {
|
|
265
|
+
return stmt.expression ? evalNode(stmt.expression, ctx) : UNRESOLVED
|
|
266
|
+
} else if (ts.isIfStatement(stmt)) {
|
|
267
|
+
const cond = evalNode(stmt.expression, ctx)
|
|
268
|
+
if (cond === UNRESOLVED) return UNRESOLVED
|
|
269
|
+
const branch = cond ? stmt.thenStatement : stmt.elseStatement
|
|
270
|
+
if (branch) {
|
|
271
|
+
const taken = evalStatementsForReturn(
|
|
272
|
+
ts.isBlock(branch) ? branch.statements : [branch],
|
|
273
|
+
ctx,
|
|
274
|
+
)
|
|
275
|
+
if (taken !== NO_RETURN) return taken
|
|
276
|
+
}
|
|
277
|
+
// Guard not taken (or its branch fell through) — continue.
|
|
278
|
+
} else {
|
|
279
|
+
// Any other statement (loop, side-effecting call) — bail.
|
|
280
|
+
return UNRESOLVED
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return NO_RETURN
|
|
284
|
+
}
|
|
285
|
+
|
|
233
286
|
function parseExpression(expr: string): ts.Expression | null {
|
|
234
287
|
// Wrap in parens so a leading `{}` parses as an object literal rather
|
|
235
288
|
// than an empty block statement.
|
|
@@ -269,23 +322,8 @@ function evalNode(node: ts.Expression, ctx: EvalContext): EvalResult {
|
|
|
269
322
|
if (!ts.isBlock(node.body)) return evalNode(node.body as ts.Expression, ctx)
|
|
270
323
|
const localBindings: Record<string, EvalResult> = { ...ctx.bindings }
|
|
271
324
|
const localCtx: EvalContext = { ...ctx, bindings: localBindings }
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
for (const d of stmt.declarationList.declarations) {
|
|
275
|
-
if (!ts.isIdentifier(d.name) || !d.initializer) continue
|
|
276
|
-
const v = evalNode(d.initializer, localCtx)
|
|
277
|
-
// Leave unresolved locals unbound; only the `return` referencing
|
|
278
|
-
// one would then surface UNRESOLVED.
|
|
279
|
-
if (v !== UNRESOLVED) localBindings[d.name.text] = v
|
|
280
|
-
}
|
|
281
|
-
} else if (ts.isReturnStatement(stmt)) {
|
|
282
|
-
return stmt.expression ? evalNode(stmt.expression, localCtx) : UNRESOLVED
|
|
283
|
-
} else {
|
|
284
|
-
// Any other statement (a branch, a side-effecting call) — bail.
|
|
285
|
-
return UNRESOLVED
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return UNRESOLVED
|
|
325
|
+
const result = evalStatementsForReturn(node.body.statements, localCtx)
|
|
326
|
+
return result === NO_RETURN ? UNRESOLVED : result
|
|
289
327
|
}
|
|
290
328
|
|
|
291
329
|
if (ts.isNumericLiteral(node)) return Number(node.text)
|