@barefootjs/jsx 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/env-signal.d.ts +40 -0
- package/dist/adapters/env-signal.d.ts.map +1 -0
- package/dist/adapters/parsed-expr-emitter.d.ts +2 -1
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/augment-inherited-props.d.ts +42 -1
- package/dist/augment-inherited-props.d.ts.map +1 -1
- package/dist/builtins.d.ts +33 -0
- package/dist/builtins.d.ts.map +1 -0
- package/dist/compiler.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +48 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +392 -26
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/compiler-stress-1244.test.ts +4 -2
- package/src/__tests__/expression-parser.test.ts +92 -1
- package/src/__tests__/ir-async.test.ts +8 -0
- package/src/__tests__/ir-builtin-import-scope.test.ts +188 -0
- package/src/__tests__/ir-region.test.ts +86 -0
- package/src/__tests__/ssr-defaults.test.ts +25 -0
- package/src/adapters/env-signal.ts +75 -0
- package/src/adapters/parsed-expr-emitter.ts +11 -0
- package/src/analyzer.ts +9 -0
- package/src/augment-inherited-props.ts +170 -2
- package/src/builtins.ts +63 -0
- package/src/compiler.ts +6 -2
- package/src/errors.ts +10 -0
- package/src/expression-parser.ts +156 -2
- package/src/index.ts +8 -2
- package/src/ir-to-client-js/imports.ts +5 -0
- package/src/jsx-to-ir.ts +189 -8
- package/src/ssr-defaults.ts +55 -17
- package/src/types.ts +16 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Shared recognition of the router v0.5 `searchParams()` environment signal
|
|
2
|
+
// for the template-string adapters (Mojo / Xslate today; Go has its own
|
|
3
|
+
// struct-field path). A request-scoped reactive env signal reads like an
|
|
4
|
+
// ordinary signal getter, but its value is a URLSearchParams-like reader with
|
|
5
|
+
// real methods (`.get(key)`) — not a hashref. The generic `member` lowering
|
|
6
|
+
// would deref `.get` as a property access and drop the call + argument, so the
|
|
7
|
+
// adapters special-case it to a real per-request reader method call. See #1922.
|
|
8
|
+
|
|
9
|
+
import type { ParsedExpr } from '../expression-parser.ts'
|
|
10
|
+
import type { IRMetadata } from '../types.ts'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The local binding name(s) that `searchParams` from `@barefootjs/client` is
|
|
14
|
+
* imported under in this component — the same import the analyzer allow-lists
|
|
15
|
+
* (`CLIENT_EXPORTS`). Usually the single name `searchParams`, but an aliased
|
|
16
|
+
* import (`import { searchParams as sp }`) binds it to `sp`, and the template
|
|
17
|
+
* expression then reads `sp()` — so adapters must gate + match against the
|
|
18
|
+
* LOCAL name(s), not the literal `searchParams`. Empty when the env signal is
|
|
19
|
+
* not imported (the component keeps the generic signal lowering).
|
|
20
|
+
*
|
|
21
|
+
* `ImportSpecifier.name` is the exported name and `alias` the local rebinding
|
|
22
|
+
* (see `collectImport` in analyzer.ts), so the import is detected by `name ===
|
|
23
|
+
* 'searchParams'` and the local binding is `alias ?? name`. Namespace / default
|
|
24
|
+
* specifiers bind a different identifier and are excluded.
|
|
25
|
+
*/
|
|
26
|
+
export function searchParamsLocalNames(metadata: IRMetadata): Set<string> {
|
|
27
|
+
const names = new Set<string>()
|
|
28
|
+
for (const imp of metadata.imports) {
|
|
29
|
+
if (imp.source !== '@barefootjs/client' || imp.isTypeOnly) continue
|
|
30
|
+
for (const s of imp.specifiers) {
|
|
31
|
+
if (s.isTypeOnly || s.isNamespace || s.isDefault) continue
|
|
32
|
+
if (s.name === 'searchParams') names.add(s.alias ?? s.name)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return names
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* True when the component imports the `searchParams` env signal under any local
|
|
40
|
+
* name. Convenience for adapters/harnesses that only need to gate on presence
|
|
41
|
+
* (the lowering itself needs the {@link searchParamsLocalNames} set to match the
|
|
42
|
+
* actual binding in the expression).
|
|
43
|
+
*/
|
|
44
|
+
export function importsSearchParams(metadata: IRMetadata): boolean {
|
|
45
|
+
return searchParamsLocalNames(metadata).size > 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Recognise a `<binding>().<method>(<args>)` env-signal method call from a
|
|
50
|
+
* `call` node's callee + args, where `<binding>` is one of the local names
|
|
51
|
+
* `searchParams` was imported under (`localNames`, from
|
|
52
|
+
* {@link searchParamsLocalNames}). Returns the method name and argument nodes
|
|
53
|
+
* when the receiver is the zero-arg env-signal getter, else null. The caller
|
|
54
|
+
* lowers the match to a real method call on its per-request reader object
|
|
55
|
+
* (`$searchParams->get('sort')` in Mojo, `$searchParams.get('sort')` in
|
|
56
|
+
* Xslate — the canonical `$searchParams` var regardless of the JS alias);
|
|
57
|
+
* without it the generic `member` lowering drops the call + arg.
|
|
58
|
+
*/
|
|
59
|
+
export function matchSearchParamsMethodCall(
|
|
60
|
+
callee: ParsedExpr,
|
|
61
|
+
args: ParsedExpr[],
|
|
62
|
+
localNames: ReadonlySet<string>,
|
|
63
|
+
): { method: string; args: ParsedExpr[] } | null {
|
|
64
|
+
if (callee.kind !== 'member' || callee.computed) return null
|
|
65
|
+
const recv = callee.object
|
|
66
|
+
if (
|
|
67
|
+
recv.kind !== 'call' ||
|
|
68
|
+
recv.args.length !== 0 ||
|
|
69
|
+
recv.callee.kind !== 'identifier' ||
|
|
70
|
+
!localNames.has(recv.callee.name)
|
|
71
|
+
) {
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
return { method: callee.property, args }
|
|
75
|
+
}
|
|
@@ -66,6 +66,7 @@ export type ArrayMethod =
|
|
|
66
66
|
| 'toLowerCase'
|
|
67
67
|
| 'toUpperCase'
|
|
68
68
|
| 'trim'
|
|
69
|
+
| 'toFixed'
|
|
69
70
|
| 'split'
|
|
70
71
|
| 'startsWith'
|
|
71
72
|
| 'endsWith'
|
|
@@ -115,6 +116,14 @@ export interface ParsedExprEmitter {
|
|
|
115
116
|
computed: boolean,
|
|
116
117
|
emit: (e: ParsedExpr) => string,
|
|
117
118
|
): string
|
|
119
|
+
// Element access with a non-literal index (`arr[index]`). The index
|
|
120
|
+
// is a full `ParsedExpr` (loop variable, arithmetic, etc.); the
|
|
121
|
+
// adapter picks array vs hash deref per target language. #1897.
|
|
122
|
+
indexAccess(
|
|
123
|
+
object: ParsedExpr,
|
|
124
|
+
index: ParsedExpr,
|
|
125
|
+
emit: (e: ParsedExpr) => string,
|
|
126
|
+
): string
|
|
118
127
|
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string
|
|
119
128
|
unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string
|
|
120
129
|
logical(
|
|
@@ -196,6 +205,8 @@ export function emitParsedExpr(expr: ParsedExpr, emitter: ParsedExprEmitter): st
|
|
|
196
205
|
return emitter.call(expr.callee, expr.args, emit)
|
|
197
206
|
case 'member':
|
|
198
207
|
return emitter.member(expr.object, expr.property, expr.computed, emit)
|
|
208
|
+
case 'index-access':
|
|
209
|
+
return emitter.indexAccess(expr.object, expr.index, emit)
|
|
199
210
|
case 'binary':
|
|
200
211
|
return emitter.binary(expr.op, expr.left, expr.right, emit)
|
|
201
212
|
case 'unary':
|
package/src/analyzer.ts
CHANGED
|
@@ -1648,6 +1648,13 @@ const CLIENT_EXPORTS = new Set([
|
|
|
1648
1648
|
'forwardProps', 'unwrap', '__slot',
|
|
1649
1649
|
'createContext', 'useContext', 'provideContext',
|
|
1650
1650
|
'createPortal', 'isSSRPortal', 'findSiblingSlot', 'cleanupPortalPlaceholder',
|
|
1651
|
+
// Request-scoped environment signal (router v0.5) — a real user-facing
|
|
1652
|
+
// reactive export the compiler lowers like any other `@barefootjs/client`
|
|
1653
|
+
// signal read (SSR: a template binding; client: a `createEffect`).
|
|
1654
|
+
'searchParams',
|
|
1655
|
+
// Compile-away JSX built-ins (#1915) — importing them is what scopes the
|
|
1656
|
+
// compiler's `<Async>` / `<Region>` recognition; the import is elided on emit.
|
|
1657
|
+
'Async', 'Region',
|
|
1651
1658
|
])
|
|
1652
1659
|
|
|
1653
1660
|
/**
|
|
@@ -1748,6 +1755,8 @@ function collectImport(node: ts.ImportDeclaration, ctx: AnalyzerContext): void {
|
|
|
1748
1755
|
alias: element.propertyName ? element.name.text : null,
|
|
1749
1756
|
isDefault: false,
|
|
1750
1757
|
isNamespace: false,
|
|
1758
|
+
// Per-specifier `import { type Foo }` — no value binding (#1915).
|
|
1759
|
+
isTypeOnly: element.isTypeOnly,
|
|
1751
1760
|
})
|
|
1752
1761
|
}
|
|
1753
1762
|
}
|
|
@@ -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
|
|