@barefootjs/jsx 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/adapters/parsed-expr-emitter.d.ts +1 -1
  2. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  3. package/dist/analyzer-context.d.ts +22 -0
  4. package/dist/analyzer-context.d.ts.map +1 -1
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/errors.d.ts +0 -9
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/expression-parser.d.ts +1 -1
  9. package/dist/expression-parser.d.ts.map +1 -1
  10. package/dist/index.js +306 -38
  11. package/dist/ir-to-client-js/compute-inlinability.d.ts +2 -2
  12. package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts +6 -18
  14. package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
  15. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
  16. package/dist/jsx-to-ir.d.ts.map +1 -1
  17. package/dist/types.d.ts +10 -3
  18. package/dist/types.d.ts.map +1 -1
  19. package/package.json +3 -3
  20. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +13 -17
  21. package/src/__tests__/circular-dependency.audit.test.ts +19 -0
  22. package/src/__tests__/compiler-stress-1244.test.ts +6 -10
  23. package/src/__tests__/component-not-found.audit.test.ts +36 -0
  24. package/src/__tests__/doc-examples.test.ts +5 -6
  25. package/src/__tests__/invalid-component-name.audit.test.ts +38 -0
  26. package/src/__tests__/invalid-jsx-attribute.audit.test.ts +47 -0
  27. package/src/__tests__/invalid-jsx-expression.audit.test.ts +44 -0
  28. package/src/__tests__/invalid-signal-usage.audit.test.ts +72 -0
  29. package/src/__tests__/jsx-function-inlining.test.ts +281 -1
  30. package/src/__tests__/loop-fallback-wrap.test.ts +5 -5
  31. package/src/__tests__/nested-loop-reactive-attrs.test.ts +4 -4
  32. package/src/__tests__/props-type-mismatch.audit.test.ts +100 -0
  33. package/src/__tests__/staged-ir/10-stage-diagnostics.test.ts +83 -2
  34. package/src/__tests__/static-loop-csr-materialize.test.ts +6 -4
  35. package/src/__tests__/type-inference-failed.audit.test.ts +48 -0
  36. package/src/__tests__/unknown-signal.audit.test.ts +111 -0
  37. package/src/adapters/parsed-expr-emitter.ts +1 -1
  38. package/src/analyzer-context.ts +25 -0
  39. package/src/analyzer.ts +207 -1
  40. package/src/errors.ts +4 -26
  41. package/src/expression-parser.ts +8 -9
  42. package/src/ir-to-client-js/compute-inlinability.ts +2 -2
  43. package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +3 -8
  44. package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +6 -17
  45. package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +5 -19
  46. package/src/jsx-to-ir.ts +215 -4
  47. package/src/types.ts +11 -3
@@ -0,0 +1,48 @@
1
+ /**
2
+ * BF030 `TYPE_INFERENCE_FAILED` deletion audit.
3
+ *
4
+ * BF030 was reserved for "Failed to infer type" — intended as a
5
+ * fallback when the compiler's `ts.Program`-based type detection
6
+ * fails. In practice, the compiler gracefully degrades (treats
7
+ * unknown types as non-reactive) and TypeScript itself reports
8
+ * type errors to the developer. No silent bug exists.
9
+ */
10
+
11
+ import { describe, test, expect } from 'bun:test'
12
+ import { analyzeComponent } from '../analyzer'
13
+ import { jsxToIR } from '../jsx-to-ir'
14
+ import { ErrorCodes } from '../errors'
15
+
16
+ function compileToIR(source: string) {
17
+ const ctx = analyzeComponent(source, '/tmp/Test.tsx')
18
+ const ir = jsxToIR(ctx)
19
+ return { ctx, ir, errors: ctx.errors }
20
+ }
21
+
22
+ describe('BF030 TYPE_INFERENCE_FAILED — deletion audit', () => {
23
+ test('component with typed props compiles without errors', () => {
24
+ const src = `
25
+ interface Props { count: number; label: string }
26
+ export function Display(props: Props) {
27
+ return <div>{props.label}: {props.count}</div>
28
+ }
29
+ `
30
+ const { errors } = compileToIR(src)
31
+ expect(errors).toHaveLength(0)
32
+ })
33
+
34
+ test('component with generic props compiles without errors', () => {
35
+ const src = `
36
+ export function List<T extends { id: string }>(props: { items: T[] }) {
37
+ return <ul>{props.items.map((item) => <li key={item.id}>{item.id}</li>)}</ul>
38
+ }
39
+ `
40
+ const { errors } = compileToIR(src)
41
+ expect(errors).toHaveLength(0)
42
+ })
43
+
44
+ test('BF030 code no longer exists in ErrorCodes', () => {
45
+ const allCodes = Object.values(ErrorCodes)
46
+ expect(allCodes).not.toContain('BF030')
47
+ })
48
+ })
@@ -0,0 +1,111 @@
1
+ /**
2
+ * BF010 `UNKNOWN_SIGNAL` deletion audit.
3
+ *
4
+ * BF010 was reserved for "Unknown signal reference" — a component that
5
+ * references a signal binding which was never declared. TypeScript's own
6
+ * `ts(2304) Cannot find name '...'` already catches this at the language
7
+ * level, so the barefoot compiler never needed a dedicated diagnostic.
8
+ *
9
+ * This file proves both claims:
10
+ * 1. TS semantic diagnostics report `ts(2304)` for the undeclared name.
11
+ * 2. The barefoot analyzer does not silently produce broken output —
12
+ * it either errors via a different code or the TS guard fires first.
13
+ */
14
+
15
+ import { describe, test, expect } from 'bun:test'
16
+ import ts from 'typescript'
17
+ import path from 'path'
18
+ import { analyzeComponent } from '../analyzer'
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Helpers — virtual TS program (mirrors reactive-type-detection.test.ts)
22
+ // ---------------------------------------------------------------------------
23
+
24
+ function getSemanticDiagnostics(source: string) {
25
+ const baseDir = path.resolve(__dirname)
26
+ const filePath = path.join(baseDir, '_unknown-signal-virtual.tsx')
27
+
28
+ const virtualFiles = new Map<string, string>()
29
+ virtualFiles.set(filePath, source)
30
+
31
+ const compilerOptions: ts.CompilerOptions = {
32
+ target: ts.ScriptTarget.Latest,
33
+ module: ts.ModuleKind.ESNext,
34
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
35
+ jsx: ts.JsxEmit.ReactJSX,
36
+ jsxImportSource: 'react',
37
+ strict: true,
38
+ noEmit: true,
39
+ skipLibCheck: true,
40
+ }
41
+
42
+ const defaultHost = ts.createCompilerHost(compilerOptions)
43
+
44
+ const host: ts.CompilerHost = {
45
+ ...defaultHost,
46
+ getSourceFile(fileName, languageVersion) {
47
+ const resolved = path.resolve(fileName)
48
+ const content = virtualFiles.get(resolved)
49
+ if (content !== undefined) {
50
+ return ts.createSourceFile(fileName, content, languageVersion, true)
51
+ }
52
+ return defaultHost.getSourceFile(fileName, languageVersion)
53
+ },
54
+ fileExists(fileName) {
55
+ const resolved = path.resolve(fileName)
56
+ if (virtualFiles.has(resolved)) return true
57
+ return defaultHost.fileExists(fileName)
58
+ },
59
+ readFile(fileName) {
60
+ const resolved = path.resolve(fileName)
61
+ const content = virtualFiles.get(resolved)
62
+ if (content !== undefined) return content
63
+ return defaultHost.readFile(fileName)
64
+ },
65
+ }
66
+
67
+ const program = ts.createProgram([filePath], compilerOptions, host)
68
+ return program.getSemanticDiagnostics(program.getSourceFile(filePath)!)
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Test cases
73
+ // ---------------------------------------------------------------------------
74
+
75
+ describe('BF010 UNKNOWN_SIGNAL — deletion audit', () => {
76
+ const undeclaredSignalSource = `
77
+ export function Counter() {
78
+ return <button onClick={() => setCount(count() + 1)}>{count()}</button>
79
+ }
80
+ `
81
+
82
+ test('TypeScript reports undeclared-name error for signal getter', () => {
83
+ const diagnostics = getSemanticDiagnostics(undeclaredSignalSource)
84
+ // ts(2304) "Cannot find name" or ts(2552) "Cannot find name … Did you mean?"
85
+ const undeclared = diagnostics.filter(d => d.code === 2304 || d.code === 2552)
86
+ // Match on the primary identifier, not a "Did you mean?" suggestion
87
+ expect(undeclared.some(d =>
88
+ ts.flattenDiagnosticMessageText(d.messageText, '\n').startsWith("Cannot find name 'count'")
89
+ )).toBe(true)
90
+ })
91
+
92
+ test('TypeScript reports undeclared-name error for signal setter', () => {
93
+ const diagnostics = getSemanticDiagnostics(undeclaredSignalSource)
94
+ const undeclared = diagnostics.filter(d => d.code === 2304 || d.code === 2552)
95
+ expect(undeclared.some(d =>
96
+ ts.flattenDiagnosticMessageText(d.messageText, '\n').startsWith("Cannot find name 'setCount'")
97
+ )).toBe(true)
98
+ })
99
+
100
+ test('barefoot analyzer produces no errors for a valid component', () => {
101
+ const src = `'use client'
102
+ import { createSignal } from '@barefootjs/client'
103
+ export function Counter() {
104
+ const [count, setCount] = createSignal(0)
105
+ return <button onClick={() => setCount(count() + 1)}>{count()}</button>
106
+ }
107
+ `
108
+ const ctx = analyzeComponent(src, '/tmp/Counter.tsx', 'Counter')
109
+ expect(ctx.errors).toHaveLength(0)
110
+ })
111
+ })
@@ -35,7 +35,7 @@
35
35
 
36
36
  import type { ParsedExpr, SortComparator, TemplatePart } from '../expression-parser'
37
37
 
38
- export type HigherOrderMethod = 'filter' | 'every' | 'some' | 'find' | 'findIndex'
38
+ export type HigherOrderMethod = 'filter' | 'every' | 'some' | 'find' | 'findIndex' | 'findLast' | 'findLastIndex'
39
39
 
40
40
  /**
41
41
  * Non-higher-order array methods (#1443). One discriminator for the
@@ -52,6 +52,28 @@ export interface PendingSignalTuple {
52
52
  initialFreeIdentifiers: ReadonlySet<string>
53
53
  }
54
54
 
55
+ /**
56
+ * One branch of a multi-return JSX helper function's if/else chain.
57
+ * `jsxReturn` is null for guard clauses that `return null`.
58
+ */
59
+ export interface MultiReturnJsxBranch {
60
+ condition: ts.Expression
61
+ jsxReturn: ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment | null
62
+ }
63
+
64
+ /**
65
+ * Extracted control flow from a multi-return JSX helper function.
66
+ * Represents `if (c1) return <A/>; if (c2) return <B/>; return <C/>`
67
+ * as a chain of condition→JSX pairs plus an optional fallback.
68
+ */
69
+ export interface MultiReturnJsxInfo {
70
+ branches: MultiReturnJsxBranch[]
71
+ fallback: ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment | null
72
+ params: string[]
73
+ /** For switch-sourced branches, the discriminant expression (e.g. `name` in `switch(name)`) */
74
+ switchDiscriminant?: ts.Expression
75
+ }
76
+
55
77
  /**
56
78
  * Represents an if statement with a JSX return in a component function.
57
79
  */
@@ -120,6 +142,8 @@ export interface AnalyzerContext {
120
142
  jsxReturn: ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment
121
143
  params: string[]
122
144
  }>
145
+ /** Maps multi-return JSX helper functions for conditional inlining at call sites. */
146
+ jsxMultiReturnFunctions: Map<string, MultiReturnJsxInfo>
123
147
  /**
124
148
  * Maps function names to reactive-factory info (#931). A reactive factory
125
149
  * is a same-file helper whose body declares reactive primitives and
@@ -220,6 +244,7 @@ export function createAnalyzerContext(
220
244
  jsxConstants: new Map(),
221
245
  inlineableJsxConsts: new Map(),
222
246
  jsxFunctions: new Map(),
247
+ jsxMultiReturnFunctions: new Map(),
223
248
  reactiveFactories: new Map(),
224
249
  signalTupleRefs: new Map(),
225
250
 
package/src/analyzer.ts CHANGED
@@ -1834,6 +1834,194 @@ function extractSingleJsxReturn(
1834
1834
  return jsxReturn
1835
1835
  }
1836
1836
 
1837
+ type JsxReturnNode = ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment
1838
+
1839
+ /**
1840
+ * Extract conditional branches from a multi-return JSX helper function body.
1841
+ * Supports if/else if chains and switch statements where every branch
1842
+ * returns JSX or null. Returns null for unsupported patterns.
1843
+ */
1844
+ function extractMultiReturnJsxBranches(
1845
+ body: ts.Block
1846
+ ): { branches: Array<{ condition: ts.Expression; jsxReturn: JsxReturnNode | null }>; fallback: JsxReturnNode | null; switchDiscriminant?: ts.Expression } | null {
1847
+ const branches: Array<{ condition: ts.Expression; jsxReturn: JsxReturnNode | null }> = []
1848
+ let fallback: JsxReturnNode | null = null
1849
+
1850
+ const stmts = body.statements
1851
+ for (let i = 0; i < stmts.length; i++) {
1852
+ const stmt = stmts[i]
1853
+
1854
+ // if (cond) return <jsx> — collect as a branch, continue to next statement
1855
+ if (ts.isIfStatement(stmt)) {
1856
+ // Walk this if/else if/else chain
1857
+ let current: ts.Statement = stmt
1858
+ while (ts.isIfStatement(current)) {
1859
+ const ifStmt = current
1860
+ // Reject branches with nested control flow — only accept
1861
+ // direct `return <jsx>` / `return null` (or a block with
1862
+ // exactly that as its only return).
1863
+ if (!isDirectReturnBlock(ifStmt.thenStatement)) return null
1864
+ const jsxReturn = findJsxReturnInBlock(ifStmt.thenStatement)
1865
+ const nullReturn = findNullReturnInBlock(ifStmt.thenStatement)
1866
+ if (!jsxReturn && !nullReturn) return null
1867
+
1868
+ branches.push({ condition: ifStmt.expression, jsxReturn: jsxReturn ?? null })
1869
+
1870
+ if (ifStmt.elseStatement) {
1871
+ if (ts.isIfStatement(ifStmt.elseStatement)) {
1872
+ current = ifStmt.elseStatement
1873
+ continue
1874
+ }
1875
+ // Final else block — chain is complete, return immediately
1876
+ // to prevent subsequent statements from overwriting fallback.
1877
+ if (!isDirectReturnBlock(ifStmt.elseStatement)) return null
1878
+ const elseJsx = findJsxReturnInBlock(ifStmt.elseStatement)
1879
+ if (elseJsx) {
1880
+ fallback = elseJsx
1881
+ } else if (!findNullReturnInBlock(ifStmt.elseStatement)) {
1882
+ return null
1883
+ }
1884
+ if (branches.length === 0) return null
1885
+ return { branches, fallback }
1886
+ }
1887
+ break
1888
+ }
1889
+ continue
1890
+ }
1891
+
1892
+ // switch (expr) { case ...: return <jsx> }
1893
+ if (ts.isSwitchStatement(stmt)) {
1894
+ // Reject mixed if+switch bodies — prior if branches would be
1895
+ // incorrectly treated as switch case expressions.
1896
+ if (branches.length > 0) return null
1897
+
1898
+ // Only inline switches whose discriminant is side-effect-free
1899
+ // (identifier or property access). A call expression like
1900
+ // `switch(getValue())` would be re-evaluated per branch in the
1901
+ // generated nested ternary.
1902
+ if (!ts.isIdentifier(stmt.expression) && !ts.isPropertyAccessExpression(stmt.expression)) {
1903
+ return null
1904
+ }
1905
+
1906
+ // Require an explicit default clause — without one, inlining
1907
+ // adds an implicit `else null` that changes runtime behavior.
1908
+ const hasDefault = stmt.caseBlock.clauses.some(c => ts.isDefaultClause(c))
1909
+ if (!hasDefault) return null
1910
+
1911
+ for (const clause of stmt.caseBlock.clauses) {
1912
+ const jsxReturn = findJsxReturnInCaseClause(clause)
1913
+ const nullReturn = findNullReturnInCaseClause(clause)
1914
+ if (!jsxReturn && !nullReturn) return null
1915
+
1916
+ if (ts.isCaseClause(clause)) {
1917
+ branches.push({
1918
+ condition: clause.expression,
1919
+ jsxReturn: jsxReturn ?? null,
1920
+ })
1921
+ } else {
1922
+ fallback = jsxReturn ?? null
1923
+ }
1924
+ }
1925
+
1926
+ if (branches.length === 0) return null
1927
+ return { branches, fallback, switchDiscriminant: stmt.expression }
1928
+ }
1929
+
1930
+ // Trailing return <jsx> or return null — this is the fallback
1931
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
1932
+ const expr = unwrapJsxTransparent(stmt.expression)
1933
+ if (ts.isJsxElement(expr) || ts.isJsxSelfClosingElement(expr) || ts.isJsxFragment(expr)) {
1934
+ fallback = expr
1935
+ } else if (expr.kind === ts.SyntaxKind.NullKeyword) {
1936
+ // fallback stays null
1937
+ } else {
1938
+ return null
1939
+ }
1940
+ continue
1941
+ }
1942
+
1943
+ // Reject bodies with variable declarations — locals referenced in
1944
+ // conditions or JSX would become undefined after inlining.
1945
+ if (ts.isVariableStatement(stmt)) return null
1946
+
1947
+ // Any other statement type → unsupported pattern
1948
+ return null
1949
+ }
1950
+
1951
+ if (branches.length === 0) return null
1952
+ return { branches, fallback }
1953
+ }
1954
+
1955
+ /**
1956
+ * Check that a statement is a direct `return ...` or a block whose
1957
+ * only non-variable statements are a single return. Rejects nested
1958
+ * control flow (nested if/switch/for/while) that could cause
1959
+ * `findJsxReturnInBlock` to pick the wrong return.
1960
+ */
1961
+ function isDirectReturnBlock(node: ts.Statement): boolean {
1962
+ if (ts.isReturnStatement(node)) return true
1963
+ if (ts.isBlock(node)) {
1964
+ let returnCount = 0
1965
+ for (const stmt of node.statements) {
1966
+ if (ts.isReturnStatement(stmt)) {
1967
+ returnCount++
1968
+ } else if (
1969
+ ts.isIfStatement(stmt) ||
1970
+ ts.isSwitchStatement(stmt) ||
1971
+ ts.isForStatement(stmt) ||
1972
+ ts.isForOfStatement(stmt) ||
1973
+ ts.isForInStatement(stmt) ||
1974
+ ts.isWhileStatement(stmt) ||
1975
+ ts.isDoStatement(stmt) ||
1976
+ ts.isTryStatement(stmt)
1977
+ ) {
1978
+ return false
1979
+ }
1980
+ }
1981
+ return returnCount === 1
1982
+ }
1983
+ return false
1984
+ }
1985
+
1986
+ function findNullReturnInBlock(node: ts.Statement): boolean {
1987
+ if (ts.isBlock(node)) {
1988
+ for (const stmt of node.statements) {
1989
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
1990
+ const expr = unwrapJsxTransparent(stmt.expression)
1991
+ if (expr.kind === ts.SyntaxKind.NullKeyword) return true
1992
+ }
1993
+ }
1994
+ }
1995
+ if (ts.isReturnStatement(node) && node.expression) {
1996
+ const expr = unwrapJsxTransparent(node.expression)
1997
+ if (expr.kind === ts.SyntaxKind.NullKeyword) return true
1998
+ }
1999
+ return false
2000
+ }
2001
+
2002
+ function findJsxReturnInCaseClause(
2003
+ clause: ts.CaseClause | ts.DefaultClause
2004
+ ): JsxReturnNode | null {
2005
+ for (const stmt of clause.statements) {
2006
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
2007
+ return extractJsxFromExpression(stmt.expression)
2008
+ }
2009
+ }
2010
+ return null
2011
+ }
2012
+
2013
+ function findNullReturnInCaseClause(
2014
+ clause: ts.CaseClause | ts.DefaultClause
2015
+ ): boolean {
2016
+ for (const stmt of clause.statements) {
2017
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
2018
+ const expr = unwrapJsxTransparent(stmt.expression)
2019
+ if (expr.kind === ts.SyntaxKind.NullKeyword) return true
2020
+ }
2021
+ }
2022
+ return false
2023
+ }
2024
+
1837
2025
  /**
1838
2026
  * Detect a multi-return JSX helper: every top-level exit point is a
1839
2027
  * `return <jsx>` or `return null`, and at least one return is JSX. Used
@@ -2002,6 +2190,15 @@ function collectFunction(
2002
2190
  jsxReturn,
2003
2191
  params: node.parameters.map(p => p.name.getText(ctx.sourceFile)),
2004
2192
  })
2193
+ } else {
2194
+ const multi = extractMultiReturnJsxBranches(node.body)
2195
+ if (multi) {
2196
+ isJsxFunction = true
2197
+ ctx.jsxMultiReturnFunctions.set(name, {
2198
+ ...multi,
2199
+ params: node.parameters.map(p => p.name.getText(ctx.sourceFile)),
2200
+ })
2201
+ }
2005
2202
  }
2006
2203
  }
2007
2204
 
@@ -2382,6 +2579,15 @@ function collectConstant(
2382
2579
  jsxReturn,
2383
2580
  params: init.parameters.map(p => p.name.getText(ctx.sourceFile)),
2384
2581
  })
2582
+ } else {
2583
+ const multi = extractMultiReturnJsxBranches(arrowBody)
2584
+ if (multi) {
2585
+ isJsxFunction = true
2586
+ ctx.jsxMultiReturnFunctions.set(name, {
2587
+ ...multi,
2588
+ params: init.parameters.map(p => p.name.getText(ctx.sourceFile)),
2589
+ })
2590
+ }
2385
2591
  }
2386
2592
  } else {
2387
2593
  // Implicit return: () => <div>...</div>
@@ -2784,7 +2990,7 @@ function inferTypeFromValue(value: string): TypeInfo {
2784
2990
  if (/\.(some|every|includes)\s*\([\s\S]*\)\s*$/.test(trimmed)) {
2785
2991
  return { kind: 'primitive', raw: 'boolean', primitive: 'boolean' }
2786
2992
  }
2787
- if (/\.(indexOf|findIndex|lastIndexOf)\s*\([\s\S]*\)\s*$/.test(trimmed)) {
2993
+ if (/\.(indexOf|findIndex|findLastIndex|lastIndexOf)\s*\([\s\S]*\)\s*$/.test(trimmed)) {
2788
2994
  return { kind: 'primitive', raw: 'number', primitive: 'number' }
2789
2995
  }
2790
2996
 
package/src/errors.ts CHANGED
@@ -19,27 +19,16 @@ export const ErrorCodes = {
19
19
  MISSING_USE_CLIENT: 'BF001',
20
20
  CLIENT_IMPORTING_SERVER: 'BF003',
21
21
 
22
- // Signal/Memo errors (BF010-BF019)
23
- UNKNOWN_SIGNAL: 'BF010',
22
+ // Signal/Memo errors (BF011-BF019)
24
23
  SIGNAL_OUTSIDE_COMPONENT: 'BF011',
25
- INVALID_SIGNAL_USAGE: 'BF012',
26
24
 
27
- // JSX errors (BF020-BF029)
28
- INVALID_JSX_EXPRESSION: 'BF020',
25
+ // JSX errors (BF021-BF029)
29
26
  UNSUPPORTED_JSX_PATTERN: 'BF021',
30
- INVALID_JSX_ATTRIBUTE: 'BF022',
31
27
  MISSING_KEY_IN_LIST: 'BF023',
32
28
  MISSING_KEY_IN_NESTED_LIST: 'BF024',
33
29
  UNSUPPORTED_DESTRUCTURE_REST: 'BF025',
34
30
 
35
- // Type errors (BF030-BF039)
36
- TYPE_INFERENCE_FAILED: 'BF030',
37
- PROPS_TYPE_MISMATCH: 'BF031',
38
-
39
- // Component errors (BF040-BF049)
40
- COMPONENT_NOT_FOUND: 'BF040',
41
- CIRCULAR_DEPENDENCY: 'BF041',
42
- INVALID_COMPONENT_NAME: 'BF042',
31
+ // Component errors (BF043-BF049)
43
32
  PROPS_DESTRUCTURING: 'BF043',
44
33
  SIGNAL_GETTER_NOT_CALLED: 'BF044',
45
34
  JSX_IN_LOCAL_FUNCTION: 'BF045',
@@ -107,16 +96,12 @@ const errorMessages: Record<ErrorCode, string> = {
107
96
  [ErrorCodes.CLIENT_IMPORTING_SERVER]:
108
97
  'Client component cannot import server component',
109
98
 
110
- [ErrorCodes.UNKNOWN_SIGNAL]: 'Unknown signal reference',
111
99
  [ErrorCodes.SIGNAL_OUTSIDE_COMPONENT]:
112
100
  'Module-level reactive declaration (createSignal / createMemo) is not allowed. ' +
113
101
  'The downstream codegen drops the declaration silently and every reference becomes a ReferenceError at SSR and at hydrate. ' +
114
102
  'Move the declaration inside a component function so each mount gets its own state.',
115
- [ErrorCodes.INVALID_SIGNAL_USAGE]: 'Invalid signal usage',
116
103
 
117
- [ErrorCodes.INVALID_JSX_EXPRESSION]: 'Invalid JSX expression',
118
104
  [ErrorCodes.UNSUPPORTED_JSX_PATTERN]: 'Unsupported JSX pattern',
119
- [ErrorCodes.INVALID_JSX_ATTRIBUTE]: 'Invalid JSX attribute',
120
105
  [ErrorCodes.MISSING_KEY_IN_LIST]:
121
106
  'Missing key attribute in list rendering. Add a key prop for efficient updates',
122
107
  [ErrorCodes.MISSING_KEY_IN_NESTED_LIST]:
@@ -130,13 +115,6 @@ const errorMessages: Record<ErrorCode, string> = {
130
115
  // stable.
131
116
  'Computed property key in .map() callback destructure is not supported. Rewrite the callback to destructure explicit bindings (e.g., `({ a, b }) => ...`) so the compiler can rewrite references to per-item signal accessors.',
132
117
 
133
- [ErrorCodes.TYPE_INFERENCE_FAILED]: 'Failed to infer type',
134
- [ErrorCodes.PROPS_TYPE_MISMATCH]: 'Props type mismatch',
135
-
136
- [ErrorCodes.COMPONENT_NOT_FOUND]: 'Component not found',
137
- [ErrorCodes.CIRCULAR_DEPENDENCY]: 'Circular dependency detected',
138
- [ErrorCodes.INVALID_COMPONENT_NAME]:
139
- 'Component name must start with uppercase letter',
140
118
  [ErrorCodes.PROPS_DESTRUCTURING]:
141
119
  'Props destructuring in function parameters breaks reactivity. Use props object directly.',
142
120
  [ErrorCodes.SIGNAL_GETTER_NOT_CALLED]:
@@ -170,7 +148,7 @@ const errorMessages: Record<ErrorCode, string> = {
170
148
  'Init-scope local referenced from template scope. The template lambda runs at module scope (via render() / renderChild()) and cannot reach init-body locals. Wrap the JSX expression in /* @client */, or lift the value to a prop or module-scope const.',
171
149
 
172
150
  [ErrorCodes.STAGE_AWAIT_IN_TEMPLATE]:
173
- 'AwaitExpression in template scope. The hydrate-time template lambda is synchronous; awaiting here would hang first render. Move the await into a server-side handler and pass the resolved value as a prop.',
151
+ 'AwaitExpression in template scope. The generated template and init functions are synchronous a bare `await` produces a SyntaxError at parse time. Move the await into the component body (before the return) or into an onMount/effect callback, and pass the resolved value to JSX.',
174
152
 
175
153
  [ErrorCodes.INLINE_JSX_CALLBACK_CAPTURE]:
176
154
  "Inline JSX-returning arrow function captures a non-module identifier. Extract the callback into a top-level 'use client' component (e.g. `function MyNode(n) { return <div/> }` then `renderNode={MyNode}`) or pass captured values via component props.",
@@ -23,7 +23,7 @@ export type ParsedExpr =
23
23
  | { kind: 'logical'; op: '&&' | '||' | '??'; left: ParsedExpr; right: ParsedExpr }
24
24
  | { kind: 'template-literal'; parts: TemplatePart[] }
25
25
  | { kind: 'arrow-fn'; param: string; body: ParsedExpr }
26
- | { kind: 'higher-order'; method: 'filter' | 'every' | 'some' | 'find' | 'findIndex'; object: ParsedExpr; param: string; predicate: ParsedExpr }
26
+ | { kind: 'higher-order'; method: 'filter' | 'every' | 'some' | 'find' | 'findIndex' | 'findLast' | 'findLastIndex'; object: ParsedExpr; param: string; predicate: ParsedExpr }
27
27
  | { kind: 'array-literal'; elements: ParsedExpr[] }
28
28
  // Non-higher-order array methods. Discriminated by `method` so each
29
29
  // adapter handles the full set via one exhaustive switch instead of
@@ -157,13 +157,12 @@ export interface SupportResult {
157
157
  // evaluate JS at runtime via hono/jsx) so this set only constrains
158
158
  // the template-language adapters.
159
159
  const UNSUPPORTED_METHODS = new Set([
160
- // Higher-order array methods. Five of these (`filter`, `every`,
161
- // `some`, `find`, `findIndex`) are intercepted as `higher-order`
162
- // IR before reaching this gate; `map` is intercepted as an
163
- // IRLoop. The rest stay refused — see #1448 Tier C for the
164
- // design questions.
160
+ // Higher-order array methods. Seven of these (`filter`, `every`,
161
+ // `some`, `find`, `findIndex`, `findLast`, `findLastIndex`) are
162
+ // intercepted as `higher-order` IR before reaching this gate;
163
+ // `map` is intercepted as an IRLoop. The rest stay refused — see
164
+ // #1448 Tier C for the design questions.
165
165
  'filter', 'map', 'reduce', 'reduceRight', 'every', 'some',
166
- 'findLast', 'findLastIndex',
167
166
  'forEach', 'flatMap', 'flat',
168
167
  // #1448 Tier A — Array methods. Each method PR adds the lowering
169
168
  // (typically a new `array-method` variant or runtime helper) and
@@ -268,12 +267,12 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
268
267
  const args = node.arguments.map(arg => convertNode(arg, raw))
269
268
 
270
269
  // Detect higher-order methods: arr.filter(x => pred), arr.every(x => pred), arr.some(x => pred)
271
- if (callee.kind === 'member' && ['filter', 'every', 'some', 'find', 'findIndex'].includes(callee.property)) {
270
+ if (callee.kind === 'member' && ['filter', 'every', 'some', 'find', 'findIndex', 'findLast', 'findLastIndex'].includes(callee.property)) {
272
271
  if (args.length === 1 && args[0].kind === 'arrow-fn') {
273
272
  const arrowFn = args[0] as { kind: 'arrow-fn'; param: string; body: ParsedExpr }
274
273
  return {
275
274
  kind: 'higher-order',
276
- method: callee.property as 'filter' | 'every' | 'some' | 'find' | 'findIndex',
275
+ method: callee.property as 'filter' | 'every' | 'some' | 'find' | 'findIndex' | 'findLast' | 'findLastIndex',
277
276
  object: callee.object,
278
277
  param: arrowFn.param,
279
278
  predicate: arrowFn.body,
@@ -554,8 +554,8 @@ function collectTemplateRiskyNames(irRoot: IRNode): Set<string> {
554
554
  * - BF061: init-scope local referenced from template scope. Same
555
555
  * fallback shape, different binding kind.
556
556
  *
557
- * BF062 (cross-stage await) belongs at the Phase 1 dispatcher (await
558
- * is a statement-level concern); not surfaced here.
557
+ * BF062 (cross-stage await) is emitted at the Phase 1 dispatcher
558
+ * (jsx-to-ir.ts) for both child and attribute positions; not here.
559
559
  *
560
560
  * Promoted from warning to error in #1187 phase 6 — the
561
561
  * `templateRiskyNames` gate in `toLegacyInlinability` ensures only
@@ -23,7 +23,7 @@ import type {
23
23
  IRNode,
24
24
  LoopParamBinding,
25
25
  } from '../../../types'
26
- import { AttrValueOf } from '../../../types'
26
+ import { AttrValueOf, pickAttrMeta } from '../../../types'
27
27
  import {
28
28
  wrapLoopParamAsAccessor,
29
29
  attrValueToString,
@@ -67,8 +67,6 @@ import type {
67
67
  InnerLoopText,
68
68
  InnerLoopsPlan,
69
69
  } from './inner-loop'
70
- import { toHtmlAttrName } from '../../utils'
71
- import { isBooleanAttr } from '../../../html-constants'
72
70
 
73
71
  export interface BuildInnerLoopsArgs {
74
72
  levels: readonly DepthLevel[]
@@ -200,14 +198,11 @@ function buildReactiveEmit(
200
198
 
201
199
  const reactiveAttrs: InnerLoopReactiveAttr[] = inner.bindings.reactiveAttrs.map(attr => {
202
200
  const wrapped = wrapLoopParamAsAccessor(wrapOuter(attr.expression), inner.param, inner.paramBindings)
203
- const isStyleObject = attr.attrName === 'style' && /^\s*\{/.test(attr.expression)
204
201
  return {
205
202
  slotId: attr.childSlotId,
206
- attrName: toHtmlAttrName(attr.attrName),
203
+ attrName: attr.attrName,
207
204
  wrappedExpression: wrapped,
208
- isStyleObject,
209
- isBoolean: isBooleanAttr(attr.attrName),
210
- presenceOrUndefined: !!attr.presenceOrUndefined,
205
+ meta: pickAttrMeta(attr),
211
206
  }
212
207
  })
213
208
 
@@ -12,6 +12,7 @@
12
12
 
13
13
  import type { LoopChildEvent } from '../../types'
14
14
  import type {
15
+ AttrMeta,
15
16
  IRLoopChildComponent,
16
17
  LoopParamBinding,
17
18
  } from '../../../types'
@@ -48,29 +49,17 @@ export interface InnerLoopText {
48
49
  * etc. — every element in the loop body whose attribute reads a signal
49
50
  * (or the loop param) gets its own per-item `createEffect`.
50
51
  *
51
- * `attrName` is already in DOM-spelling (kebab for SVG presentation
52
- * attrs, `class` for `className`) so the stringifier can emit
53
- * `setAttribute(attrName, ...)` directly.
52
+ * `attrName` is in JSX form (e.g. `className`, not `class`) — the
53
+ * stringifier delegates to `emitAttrUpdate` which maps to HTML spelling.
54
54
  */
55
55
  export interface InnerLoopReactiveAttr {
56
56
  slotId: string
57
- /** DOM attribute name (already mapped via `toHtmlAttrName`). */
57
+ /** JSX attribute name (mapped to HTML spelling by `emitAttrUpdate`). */
58
58
  attrName: string
59
59
  /** Already wrapped via inner+outer loop param accessor. */
60
60
  wrappedExpression: string
61
- /** True for `style={{...}}` object literals — routed through `styleToCss`. */
62
- isStyleObject: boolean
63
- /** True for boolean DOM attributes (`disabled`, `checked`, ...). */
64
- isBoolean: boolean
65
- /**
66
- * True for `attr={expr || undefined}` patterns where the compiler
67
- * stripped the `|| undefined` and now stores the bare expression.
68
- * The emitter must use a truthy check (not `!= null`) — otherwise a
69
- * concrete `false` value writes `data-attr="false"` instead of
70
- * removing the attribute. Surfaced by the calendar demo's
71
- * `data-outside={day.isOutside || undefined}` on a nested map root.
72
- */
73
- presenceOrUndefined: boolean
61
+ /** Pre-copied attr metadata used by `emitAttrUpdate`. */
62
+ meta: AttrMeta
74
63
  }
75
64
 
76
65
  /**