@barefootjs/jsx 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/env-signal.d.ts +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 +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +411 -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/profiler.d.ts +37 -0
- package/dist/profiler.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__/profiler.test.ts +69 -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 +5 -2
- package/src/ir-to-client-js/imports.ts +5 -0
- package/src/jsx-to-ir.ts +189 -8
- package/src/profiler.ts +63 -0
- package/src/ssr-defaults.ts +55 -17
- package/src/types.ts +16 -0
|
@@ -164,7 +164,31 @@ export function augmentInheritedPropAccesses(ir: ComponentIR): void {
|
|
|
164
164
|
if (!node) return
|
|
165
165
|
const el = node as unknown as IRElement
|
|
166
166
|
for (const attr of el.attrs ?? []) {
|
|
167
|
-
const v = attr.value as {
|
|
167
|
+
const v = attr.value as {
|
|
168
|
+
kind?: string
|
|
169
|
+
expr?: string
|
|
170
|
+
presenceOrUndefined?: boolean
|
|
171
|
+
parts?: Array<
|
|
172
|
+
| { type: 'string'; value: string }
|
|
173
|
+
| { type: 'ternary'; condition: string; whenTrue: string; whenFalse: string }
|
|
174
|
+
| { type: 'lookup'; key: string }
|
|
175
|
+
>
|
|
176
|
+
}
|
|
177
|
+
// Template-literal attr values (`className={\`… \${props.className ??
|
|
178
|
+
// ''}\`}`) carry their `props.X` reads inside the parts structure,
|
|
179
|
+
// not a flat expr string (#1896 — TabsContent's template referenced
|
|
180
|
+
// `.ClassName` while the Props struct never declared it). Scan every
|
|
181
|
+
// textual field of every part shape.
|
|
182
|
+
if (v?.parts) {
|
|
183
|
+
for (const part of v.parts) {
|
|
184
|
+
if (part.type === 'string') scan(part.value)
|
|
185
|
+
else if (part.type === 'ternary') {
|
|
186
|
+
scan(part.condition)
|
|
187
|
+
scan(part.whenTrue)
|
|
188
|
+
scan(part.whenFalse)
|
|
189
|
+
} else if (part.type === 'lookup') scan(part.key)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
168
192
|
if (v?.kind === 'expression' && typeof v.expr === 'string') {
|
|
169
193
|
scan(v.expr)
|
|
170
194
|
const expr = v.expr.trim()
|
|
@@ -186,6 +210,20 @@ export function augmentInheritedPropAccesses(ir: ComponentIR): void {
|
|
|
186
210
|
const c = child as { element?: IRNode }
|
|
187
211
|
walk((c.element ?? child) as IRNode)
|
|
188
212
|
}
|
|
213
|
+
// Conditional / if-statement nodes keep their subtrees in branch
|
|
214
|
+
// fields, not `children` (#1896 — DialogTrigger's asChild
|
|
215
|
+
// if-statement hid the button branch's `id={props.id}` from this
|
|
216
|
+
// scan, so the template referenced `.ID` without a Props field).
|
|
217
|
+
const branchy = node as unknown as {
|
|
218
|
+
whenTrue?: IRNode
|
|
219
|
+
whenFalse?: IRNode
|
|
220
|
+
consequent?: IRNode
|
|
221
|
+
alternate?: IRNode
|
|
222
|
+
}
|
|
223
|
+
walk(branchy.whenTrue)
|
|
224
|
+
walk(branchy.whenFalse)
|
|
225
|
+
walk(branchy.consequent)
|
|
226
|
+
walk(branchy.alternate)
|
|
189
227
|
}
|
|
190
228
|
walk(ir.root)
|
|
191
229
|
|
|
@@ -206,6 +244,134 @@ export function augmentInheritedPropAccesses(ir: ComponentIR): void {
|
|
|
206
244
|
}
|
|
207
245
|
}
|
|
208
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Parse a const initializer to its static string value: a string literal,
|
|
249
|
+
* a no-substitution template literal, or a `[<string literals>].join(sep)`
|
|
250
|
+
* call. Returns `null` for anything else. The TS parser resolves escapes
|
|
251
|
+
* and quoting exactly as JS would, matching the value the Hono reference
|
|
252
|
+
* inlines at runtime. Shared by the Go, Mojo, and Xslate adapters.
|
|
253
|
+
*/
|
|
254
|
+
export function parseStaticStringConst(source: string): string | null {
|
|
255
|
+
const sf = ts.createSourceFile(
|
|
256
|
+
'__const.ts', `const __x = (${source});`, ts.ScriptTarget.Latest, /*setParentNodes*/ false,
|
|
257
|
+
)
|
|
258
|
+
const stmt = sf.statements[0]
|
|
259
|
+
if (!stmt || !ts.isVariableStatement(stmt)) return null
|
|
260
|
+
let init = stmt.declarationList.declarations[0]?.initializer
|
|
261
|
+
while (init && ts.isParenthesizedExpression(init)) init = init.expression
|
|
262
|
+
if (!init) return null
|
|
263
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
264
|
+
return init.text
|
|
265
|
+
}
|
|
266
|
+
return evalStringArrayJoin(source)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Statically evaluate a template literal whose every interpolation is a
|
|
271
|
+
* bare identifier present in `resolved` to its flat string, else `null`.
|
|
272
|
+
* Companion to `parseStaticStringConst` for COMPOSED module consts
|
|
273
|
+
* (#1896 / #1897 — radio-group's
|
|
274
|
+
* `itemClasses = \`\${itemBaseClasses} \${itemFocusClasses} …\``).
|
|
275
|
+
*/
|
|
276
|
+
export function evalTemplateOfStringConsts(
|
|
277
|
+
source: string,
|
|
278
|
+
resolved: ReadonlyMap<string, string>,
|
|
279
|
+
): string | null {
|
|
280
|
+
const sf = ts.createSourceFile(
|
|
281
|
+
'__const.ts', `const __x = (${source});`, ts.ScriptTarget.Latest, /*setParentNodes*/ false,
|
|
282
|
+
)
|
|
283
|
+
const stmt = sf.statements[0]
|
|
284
|
+
if (!stmt || !ts.isVariableStatement(stmt)) return null
|
|
285
|
+
let init = stmt.declarationList.declarations[0]?.initializer
|
|
286
|
+
while (init && ts.isParenthesizedExpression(init)) init = init.expression
|
|
287
|
+
if (!init || !ts.isTemplateExpression(init)) return null
|
|
288
|
+
let out = init.head.text
|
|
289
|
+
for (const span of init.templateSpans) {
|
|
290
|
+
if (!ts.isIdentifier(span.expression)) return null
|
|
291
|
+
const value = resolved.get(span.expression.text)
|
|
292
|
+
if (value === undefined) return null
|
|
293
|
+
out += value + span.literal.text
|
|
294
|
+
}
|
|
295
|
+
return out
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Build the module string-const map from the IR's localConstants —
|
|
300
|
+
* the SINGLE SOURCE OF TRUTH for all three SSR template adapters.
|
|
301
|
+
* A const qualifies when module-scope and statically resolvable as:
|
|
302
|
+
* - a pure string / no-substitution-template literal,
|
|
303
|
+
* - `[<string literals>].join(sep)`,
|
|
304
|
+
* - a template literal COMPOSED of other qualifying module consts
|
|
305
|
+
* (resolved to a fixed point, so composition order doesn't matter).
|
|
306
|
+
*/
|
|
307
|
+
export function collectModuleStringConsts(
|
|
308
|
+
constants: IRMetadata['localConstants'] | undefined,
|
|
309
|
+
): Map<string, string> {
|
|
310
|
+
const map = new Map<string, string>()
|
|
311
|
+
const candidates = (constants ?? []).filter(
|
|
312
|
+
c => c.isModule && c.value !== undefined,
|
|
313
|
+
)
|
|
314
|
+
let progressed = true
|
|
315
|
+
while (progressed) {
|
|
316
|
+
progressed = false
|
|
317
|
+
for (const c of candidates) {
|
|
318
|
+
if (map.has(c.name)) continue
|
|
319
|
+
const literal =
|
|
320
|
+
parseStaticStringConst(c.value!) ??
|
|
321
|
+
evalTemplateOfStringConsts(c.value!, map)
|
|
322
|
+
if (literal !== null) {
|
|
323
|
+
map.set(c.name, literal)
|
|
324
|
+
progressed = true
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return map
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Resolve `IDENT.key` / `IDENT['key']` where `IDENT` is a module-scope
|
|
333
|
+
* object-literal const and the key/value are static literals — a fully
|
|
334
|
+
* compile-time lookup (the icon registry's `strokePaths['chevron-down']`,
|
|
335
|
+
* pagination's `variantClasses.ghost`; #1896 / #1897). Returns the
|
|
336
|
+
* looked-up scalar, or `null` for any other shape so callers fall back
|
|
337
|
+
* to their generic lowering. Shared by all three SSR template adapters;
|
|
338
|
+
* the prop-KEYED variant of the pattern lives in `parseRecordIndexAccess`.
|
|
339
|
+
*/
|
|
340
|
+
export function lookupStaticRecordLiteral(
|
|
341
|
+
objectName: string,
|
|
342
|
+
key: string,
|
|
343
|
+
constants: IRMetadata['localConstants'] | undefined,
|
|
344
|
+
): { kind: 'string' | 'number'; text: string } | null {
|
|
345
|
+
const constInfo = (constants ?? []).find(c => c.name === objectName && c.isModule)
|
|
346
|
+
if (constInfo?.value === undefined) return null
|
|
347
|
+
const sf = ts.createSourceFile(
|
|
348
|
+
'__rec.ts', `(${constInfo.value})`, ts.ScriptTarget.Latest, /*setParentNodes*/ true,
|
|
349
|
+
)
|
|
350
|
+
if (sf.statements.length !== 1) return null
|
|
351
|
+
const stmt = sf.statements[0]
|
|
352
|
+
if (!ts.isExpressionStatement(stmt)) return null
|
|
353
|
+
let parsed: ts.Expression = stmt.expression
|
|
354
|
+
while (ts.isParenthesizedExpression(parsed)) parsed = parsed.expression
|
|
355
|
+
if (!ts.isObjectLiteralExpression(parsed)) return null
|
|
356
|
+
for (const prop of parsed.properties) {
|
|
357
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
358
|
+
const name = prop.name
|
|
359
|
+
const propKey =
|
|
360
|
+
ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name)
|
|
361
|
+
? name.text
|
|
362
|
+
: null
|
|
363
|
+
if (propKey !== key) continue
|
|
364
|
+
let v: ts.Expression = prop.initializer
|
|
365
|
+
while (ts.isParenthesizedExpression(v)) v = v.expression
|
|
366
|
+
if (ts.isNumericLiteral(v)) return { kind: 'number', text: v.text }
|
|
367
|
+
if (ts.isStringLiteral(v) || ts.isNoSubstitutionTemplateLiteral(v)) {
|
|
368
|
+
return { kind: 'string', text: v.text }
|
|
369
|
+
}
|
|
370
|
+
return null
|
|
371
|
+
}
|
|
372
|
+
return null
|
|
373
|
+
}
|
|
374
|
+
|
|
209
375
|
/**
|
|
210
376
|
* Statically evaluate `[<string literals>].join(<sep?>)` (e.g. a module-scope
|
|
211
377
|
* `const stateClasses = ['…', …].join(' ')`) to its joined string, so SSR
|
|
@@ -213,7 +379,9 @@ export function augmentInheritedPropAccesses(ir: ComponentIR): void {
|
|
|
213
379
|
* instead of referencing a binding that doesn't exist server-side. Default
|
|
214
380
|
* separator `,` matches JS `Array.prototype.join`. Returns `null` for any
|
|
215
381
|
* other shape (non-`.join` call, non-array receiver, non-string-literal element
|
|
216
|
-
* or separator). Shared by the Mojo
|
|
382
|
+
* or separator). Shared by the Go, Mojo, and Xslate adapters (all three
|
|
383
|
+
* resolve module consts through `collectModuleStringConsts` above, which
|
|
384
|
+
* folds these joins during its fixed-point pass).
|
|
217
385
|
*/
|
|
218
386
|
export function evalStringArrayJoin(source: string): string | null {
|
|
219
387
|
const sf = ts.createSourceFile(
|
package/src/builtins.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler built-in JSX tags that are import-scoped to `@barefootjs/client`
|
|
3
|
+
* and compiled away (no runtime value survives in emitted output).
|
|
4
|
+
*
|
|
5
|
+
* `<Async>` and `<Region>` are recognised **structurally** — by their
|
|
6
|
+
* `@barefootjs/client` import in `ir.metadata.imports`, never by a bare
|
|
7
|
+
* capitalized tag-name match — so a user's own `<Async>` / `<Region>`
|
|
8
|
+
* component (imported from elsewhere or declared locally) does not collide
|
|
9
|
+
* with the built-in. The import is elided on emit (both `templateImports`
|
|
10
|
+
* and the client-JS DOM imports) so it never lingers as a phantom runtime
|
|
11
|
+
* import.
|
|
12
|
+
*
|
|
13
|
+
* Runtime stubs + types ship from `@barefootjs/client` (see
|
|
14
|
+
* `packages/client/src/builtins.ts`). See piconic-ai/barefootjs#1915.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ImportInfo } from './types.ts'
|
|
18
|
+
|
|
19
|
+
/** Package that the built-in tags must be imported from to be recognised. */
|
|
20
|
+
export const CLIENT_BUILTIN_SOURCE = '@barefootjs/client'
|
|
21
|
+
|
|
22
|
+
export type ClientBuiltinTag = 'Async' | 'Region'
|
|
23
|
+
|
|
24
|
+
/** The recognised built-in tag (export) names. */
|
|
25
|
+
export const CLIENT_BUILTIN_TAGS: readonly ClientBuiltinTag[] = ['Async', 'Region']
|
|
26
|
+
|
|
27
|
+
/** True when `name` is one of the compile-away built-in export names. */
|
|
28
|
+
export function isClientBuiltinName(name: string): name is ClientBuiltinTag {
|
|
29
|
+
return name === 'Async' || name === 'Region'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Elide the compile-away built-ins from an import list for emission (#1915).
|
|
34
|
+
* `<Async>` / `<Region>` are lowered into the template, so their
|
|
35
|
+
* `@barefootjs/client` import must not survive as a phantom runtime import in
|
|
36
|
+
* either the SSR template or the client JS bundle. Drops the `Async` / `Region`
|
|
37
|
+
* specifiers from `@barefootjs/client` imports, and drops the whole import
|
|
38
|
+
* statement when it has no remaining specifiers.
|
|
39
|
+
*/
|
|
40
|
+
export function stripClientBuiltinImports(imports: ImportInfo[]): ImportInfo[] {
|
|
41
|
+
const result: ImportInfo[] = []
|
|
42
|
+
for (const imp of imports) {
|
|
43
|
+
// Only a *value* named import of the built-ins can become a phantom runtime
|
|
44
|
+
// import. Leave everything else untouched: imports from other sources;
|
|
45
|
+
// `import type { Async }` (erased by TS — never a runtime import, and may be
|
|
46
|
+
// needed to type-check emitted templates); and side-effect imports (no
|
|
47
|
+
// specifiers, deliberate). See #1915 review.
|
|
48
|
+
if (imp.source !== CLIENT_BUILTIN_SOURCE || imp.isTypeOnly || imp.specifiers.length === 0) {
|
|
49
|
+
result.push(imp)
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
const kept = imp.specifiers.filter(
|
|
53
|
+
// Keep per-specifier type-only built-ins (`import { type Async }`) — they
|
|
54
|
+
// are erased by TS and never a runtime phantom.
|
|
55
|
+
spec => spec.isDefault || spec.isNamespace || spec.isTypeOnly || !isClientBuiltinName(spec.name),
|
|
56
|
+
)
|
|
57
|
+
// Drop the import entirely when every specifier was a built-in; otherwise
|
|
58
|
+
// re-emit without them.
|
|
59
|
+
if (kept.length === 0) continue
|
|
60
|
+
result.push(kept.length === imp.specifiers.length ? imp : { ...imp, specifiers: kept })
|
|
61
|
+
}
|
|
62
|
+
return result
|
|
63
|
+
}
|
package/src/compiler.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
import type { TemplateAdapter } from './adapters/interface.ts'
|
|
15
15
|
import { analyzeComponent, listComponentFunctions, createProgramForFile, needsTypeBasedDetection } from './analyzer.ts'
|
|
16
16
|
import { jsxToIR } from './jsx-to-ir.ts'
|
|
17
|
+
import { stripClientBuiltinImports } from './builtins.ts'
|
|
17
18
|
import { generateClientJs, generateClientJsWithSourceMap, analyzeClientNeeds } from './ir-to-client-js/index.ts'
|
|
18
19
|
import { emitModuleLevelDeclarations } from './ir-to-client-js/emit-module-level.ts'
|
|
19
20
|
import { RUNTIME_MODULE, detectUsedImports as detectUsedImportsFromCode } from './ir-to-client-js/imports.ts'
|
|
@@ -502,8 +503,11 @@ export function buildMetadata(
|
|
|
502
503
|
// re-emission. Adapters that re-emit imports (Hono, test) call
|
|
503
504
|
// `rewriteImportsForTemplate` themselves to apply client-shim rewrite or
|
|
504
505
|
// strip behaviour; adapters whose templates never carry imports (Go,
|
|
505
|
-
// Mojo) only consult this list for diagnostics like BF103.
|
|
506
|
-
|
|
506
|
+
// Mojo) only consult this list for diagnostics like BF103. The
|
|
507
|
+
// compile-away built-ins (`<Async>` / `<Region>`) are stripped here so
|
|
508
|
+
// their `@barefootjs/client` import never reaches any adapter's template
|
|
509
|
+
// as a phantom (#1915).
|
|
510
|
+
templateImports: stripClientBuiltinImports(ctx.imports),
|
|
507
511
|
namedExports: ctx.namedExports,
|
|
508
512
|
localFunctions: ctx.localFunctions,
|
|
509
513
|
localConstants: ctx.localConstants,
|
package/src/errors.ts
CHANGED
|
@@ -47,6 +47,11 @@ export const ErrorCodes = {
|
|
|
47
47
|
// Import errors (BF050-BF059)
|
|
48
48
|
SHARED_PROGRAM_REQUIRED: 'BF050',
|
|
49
49
|
WRONG_PACKAGE_IMPORT: 'BF051',
|
|
50
|
+
// A bare `<Async>` / `<Region>` tag was used without importing it from
|
|
51
|
+
// `@barefootjs/client`. The built-ins are recognised import-scoped (#1915),
|
|
52
|
+
// so an unimported tag with the built-in name is either a forgotten import
|
|
53
|
+
// or an undeclared component — fail loud with the import to add.
|
|
54
|
+
BUILTIN_REQUIRES_IMPORT: 'BF054',
|
|
50
55
|
|
|
51
56
|
// Init statement errors (BF052)
|
|
52
57
|
UNDECLARED_INIT_STATEMENT_REFERENCE: 'BF052',
|
|
@@ -135,6 +140,11 @@ const errorMessages: Record<ErrorCode, string> = {
|
|
|
135
140
|
[ErrorCodes.WRONG_PACKAGE_IMPORT]:
|
|
136
141
|
'Import from wrong package.',
|
|
137
142
|
|
|
143
|
+
[ErrorCodes.BUILTIN_REQUIRES_IMPORT]:
|
|
144
|
+
"Built-in <Async> / <Region> must be imported from '@barefootjs/client'. " +
|
|
145
|
+
'The compiler recognises these tags by their import (not by tag name), ' +
|
|
146
|
+
'so an unimported tag with this name is treated as an undeclared component.',
|
|
147
|
+
|
|
138
148
|
[ErrorCodes.UNDECLARED_INIT_STATEMENT_REFERENCE]:
|
|
139
149
|
'Init statement references an undeclared identifier. Declare it at module scope, inside the component, or import it — otherwise ESM strict mode throws ReferenceError at runtime.',
|
|
140
150
|
|
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'
|
|
@@ -247,7 +248,7 @@ export {
|
|
|
247
248
|
export { ErrorCodes, createError, formatError, generateCodeFrame } from './errors.ts'
|
|
248
249
|
|
|
249
250
|
// Expression Parser
|
|
250
|
-
export { parseExpression, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries } from './expression-parser.ts'
|
|
251
|
+
export { parseExpression, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries, parseProviderObjectLiteral, type ProviderObjectMember } from './expression-parser.ts'
|
|
251
252
|
export type { StyleObjectEntry } from './expression-parser.ts'
|
|
252
253
|
export type { ParsedExpr, ParsedStatement, SortComparator, SortKey, ReduceOp, FlatDepth, FlatMapOp, FlatMapLeaf, SupportLevel, SupportResult, TemplatePart } from './expression-parser.ts'
|
|
253
254
|
export { buildLoopChainExpr } from './loop-chain.ts'
|
|
@@ -285,6 +286,7 @@ export type { WrapReason } from './ir-to-client-js/reactivity.ts'
|
|
|
285
286
|
// Reactive performance profiler (#1690). Static half (SR5 budget, SR6 diff) +
|
|
286
287
|
// dynamic half (SR2/SR4 join, SR7 report, v1 analyses).
|
|
287
288
|
export {
|
|
289
|
+
PROFILE_SCHEMA_VERSION,
|
|
288
290
|
buildStaticBudget,
|
|
289
291
|
formatStaticBudget,
|
|
290
292
|
diffStaticBudget,
|
|
@@ -306,6 +308,7 @@ export type {
|
|
|
306
308
|
StaticBudget,
|
|
307
309
|
StaticBudgetOptions,
|
|
308
310
|
FanOutEntry,
|
|
311
|
+
BudgetHandler,
|
|
309
312
|
BudgetDiff,
|
|
310
313
|
FanOutChange,
|
|
311
314
|
ProfileReport,
|
|
@@ -352,7 +355,7 @@ export type {
|
|
|
352
355
|
export { BOOLEAN_ATTRS, isBooleanAttr } from './html-constants.ts'
|
|
353
356
|
|
|
354
357
|
// Shared props-object-pattern helpers for the Go / Mojo template adapters
|
|
355
|
-
export { augmentInheritedPropAccesses, parseRecordIndexAccess, evalStringArrayJoin, collectContextConsumers } from './augment-inherited-props.ts'
|
|
358
|
+
export { augmentInheritedPropAccesses, parseRecordIndexAccess, evalStringArrayJoin, collectModuleStringConsts, lookupStaticRecordLiteral, collectContextConsumers } from './augment-inherited-props.ts'
|
|
356
359
|
export type { RecordIndexAccess, RecordIndexEntry, ContextConsumer } from './augment-inherited-props.ts'
|
|
357
360
|
|
|
358
361
|
// 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
|
}
|