@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.
- package/dist/adapters/parsed-expr-emitter.d.ts +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts +22 -0
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/errors.d.ts +0 -9
- package/dist/errors.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +1 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.js +306 -38
- package/dist/ir-to-client-js/compute-inlinability.d.ts +2 -2
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts +6 -18
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/types.d.ts +10 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +13 -17
- package/src/__tests__/circular-dependency.audit.test.ts +19 -0
- package/src/__tests__/compiler-stress-1244.test.ts +6 -10
- package/src/__tests__/component-not-found.audit.test.ts +36 -0
- package/src/__tests__/doc-examples.test.ts +5 -6
- package/src/__tests__/invalid-component-name.audit.test.ts +38 -0
- package/src/__tests__/invalid-jsx-attribute.audit.test.ts +47 -0
- package/src/__tests__/invalid-jsx-expression.audit.test.ts +44 -0
- package/src/__tests__/invalid-signal-usage.audit.test.ts +72 -0
- package/src/__tests__/jsx-function-inlining.test.ts +281 -1
- package/src/__tests__/loop-fallback-wrap.test.ts +5 -5
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +4 -4
- package/src/__tests__/props-type-mismatch.audit.test.ts +100 -0
- package/src/__tests__/staged-ir/10-stage-diagnostics.test.ts +83 -2
- package/src/__tests__/static-loop-csr-materialize.test.ts +6 -4
- package/src/__tests__/type-inference-failed.audit.test.ts +48 -0
- package/src/__tests__/unknown-signal.audit.test.ts +111 -0
- package/src/adapters/parsed-expr-emitter.ts +1 -1
- package/src/analyzer-context.ts +25 -0
- package/src/analyzer.ts +207 -1
- package/src/errors.ts +4 -26
- package/src/expression-parser.ts +8 -9
- package/src/ir-to-client-js/compute-inlinability.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +3 -8
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +6 -17
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +5 -19
- package/src/jsx-to-ir.ts +215 -4
- 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
|
package/src/analyzer-context.ts
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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
|
-
//
|
|
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
|
|
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.",
|
package/src/expression-parser.ts
CHANGED
|
@@ -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.
|
|
161
|
-
// `some`, `find`, `findIndex`) are
|
|
162
|
-
// IR before reaching this gate;
|
|
163
|
-
// IRLoop. The rest stay refused — see
|
|
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)
|
|
558
|
-
*
|
|
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:
|
|
203
|
+
attrName: attr.attrName,
|
|
207
204
|
wrappedExpression: wrapped,
|
|
208
|
-
|
|
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
|
|
52
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
62
|
-
|
|
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
|
/**
|