@barefootjs/jsx 0.18.4 → 0.18.7
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/dangerous-inner-html.d.ts +77 -0
- package/dist/adapters/dangerous-inner-html.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts +22 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -0
- package/dist/adapters/parsed-expr-emitter.d.ts +2 -2
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +2 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1108 -307
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- 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/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +45 -3
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts +38 -0
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +24 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +42 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts +20 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts +1 -1
- package/dist/ir-to-client-js/types.d.ts +30 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +25 -0
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/signal-init-eval.d.ts +82 -0
- package/dist/signal-init-eval.d.ts.map +1 -0
- package/dist/static-literal.d.ts +45 -0
- package/dist/static-literal.d.ts.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +19 -13
- package/src/__tests__/create-selector.test.ts +110 -0
- package/src/__tests__/csr-template-loop-shadowing.test.ts +214 -0
- package/src/__tests__/dangerous-inner-html-resolver.test.ts +197 -0
- package/src/__tests__/event-delegation-index-param.test.ts +130 -0
- package/src/__tests__/expression-parser.test.ts +38 -0
- package/src/__tests__/inner-loop-svg-namespace.test.ts +245 -0
- package/src/__tests__/ir-walker.test.ts +1 -0
- package/src/__tests__/loop-bound-names.test.ts +102 -0
- package/src/__tests__/loop-plan-classification.test.ts +2 -0
- package/src/__tests__/map-function-reference.test.ts +303 -0
- package/src/__tests__/materialize-getter-calls.test.ts +1 -0
- package/src/__tests__/nested-loop-index-param.test.ts +285 -0
- package/src/__tests__/signal-init-eval.test.ts +138 -0
- package/src/__tests__/static-array-inner-loop-index-param.test.ts +180 -0
- package/src/__tests__/static-literal.test.ts +126 -0
- package/src/__tests__/string-concat-identifier.test.ts +55 -0
- package/src/adapters/dangerous-inner-html.ts +186 -0
- package/src/adapters/loop-bound-names.ts +80 -0
- package/src/adapters/parsed-expr-emitter.ts +19 -1
- package/src/analyzer.ts +6 -0
- package/src/expression-parser.ts +59 -25
- package/src/index.ts +19 -3
- package/src/ir-to-client-js/collect-elements.ts +18 -3
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +21 -4
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +13 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +10 -0
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +9 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/shared.ts +103 -6
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +31 -7
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +11 -2
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +5 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +53 -10
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +25 -4
- package/src/ir-to-client-js/control-flow/stringify/skeleton-paths.ts +70 -0
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +25 -1
- package/src/ir-to-client-js/csr-substitute.ts +54 -1
- package/src/ir-to-client-js/html-template.ts +292 -13
- package/src/ir-to-client-js/imports.ts +1 -0
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +12 -1
- package/src/ir-to-client-js/plan/static-array-child-init.ts +20 -1
- package/src/ir-to-client-js/reactivity.ts +6 -0
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +11 -6
- package/src/ir-to-client-js/types.ts +31 -0
- package/src/ir-to-client-js/utils.ts +34 -1
- package/src/jsx-to-ir.ts +322 -48
- package/src/signal-init-eval.ts +165 -0
- package/src/static-literal.ts +128 -0
- package/src/types.ts +68 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `dangerouslySetInnerHTML={{ __html: expr }}` recognition + policy
|
|
3
|
+
* (#2207). Single place every template adapter calls from `renderElement`,
|
|
4
|
+
* so the injection-safety-relevant policy — "only a compile-time string
|
|
5
|
+
* literal is lowered; anything else refuses loudly" — lives in exactly one
|
|
6
|
+
* reviewable spot instead of being re-derived independently in 8 adapters.
|
|
7
|
+
*
|
|
8
|
+
* Scope, deliberately narrow for v1 (see #2207 / #2215 for the follow-up):
|
|
9
|
+
* a static literal is spliced directly into the adapter's OWN template
|
|
10
|
+
* source as trusted text (same trust domain as hand-writing the HTML into
|
|
11
|
+
* the template) — never routed through a `|safe`/`|raw`/`{!! !!}`-style
|
|
12
|
+
* runtime raw-output primitive, which would reopen a template-source
|
|
13
|
+
* injection surface for no benefit (the value is already fully known at
|
|
14
|
+
* compile time). A DYNAMIC value (signal, prop, a template literal WITH a
|
|
15
|
+
* substitution, local const, anything non-literal) is refused with
|
|
16
|
+
* `BF101` — a no-substitution template literal is NOT dynamic; the parser
|
|
17
|
+
* normalizes it to the same `{kind:'literal', literalType:'string'}` shape
|
|
18
|
+
* as a plain string literal, so it's treated identically (see
|
|
19
|
+
* `staticHtmlLiteral` below). Hono/CSR already support it (the client
|
|
20
|
+
* drives a `createEffect`-based `el.innerHTML = …`
|
|
21
|
+
* assignment — see `ir-to-client-js/emit-reactive.ts`), so this is a
|
|
22
|
+
* template-adapter-only gap, tracked separately (#2215) rather than folded
|
|
23
|
+
* into this literal-only cut.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { CompilerError, IRAttribute, IRElement, SourceLocation } from '../types.ts'
|
|
27
|
+
import { parseExpression, type ParsedExpr } from '../expression-parser.ts'
|
|
28
|
+
|
|
29
|
+
const DANGEROUS_INNER_HTML_ATTR = 'dangerouslySetInnerHTML'
|
|
30
|
+
|
|
31
|
+
export function isDangerousInnerHtmlAttr(attr: IRAttribute): boolean {
|
|
32
|
+
return attr.name === DANGEROUS_INNER_HTML_ATTR
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type DangerousInnerHtmlResolution =
|
|
36
|
+
| { kind: 'static'; html: string }
|
|
37
|
+
| { kind: 'dynamic'; expr: string; loc: SourceLocation }
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve `element`'s `dangerouslySetInnerHTML` attribute, if present.
|
|
41
|
+
* Returns `null` when the attribute is absent (the overwhelmingly common
|
|
42
|
+
* case — callers should treat `null` as "render this element normally") —
|
|
43
|
+
* or when it carries `/* @client *\/` (Fable review, #2217): a `clientOnly`
|
|
44
|
+
* attr is already deferred to hydrate by every adapter's `renderAttributes`
|
|
45
|
+
* (`if (attr.clientOnly) continue`, unrelated to this module), which is
|
|
46
|
+
* itself a working escape hatch for a dynamic `__html` — the client's
|
|
47
|
+
* `createEffect`-driven `el.innerHTML = …` assignment (`emit-reactive.ts`)
|
|
48
|
+
* runs regardless of what SSR does. Treating `clientOnly` as "render this
|
|
49
|
+
* element normally" here (rather than refusing with BF101) preserves that
|
|
50
|
+
* escape hatch instead of regressing it.
|
|
51
|
+
* A present-but-non-`expression` value (e.g. a bare boolean/spread — not a
|
|
52
|
+
* shape `{ __html }` can ever legitimately take) is treated as `dynamic`
|
|
53
|
+
* so it refuses rather than silently doing nothing.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveDangerousInnerHtml(element: IRElement): DangerousInnerHtmlResolution | null {
|
|
56
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr)
|
|
57
|
+
if (!attr) return null
|
|
58
|
+
if (attr.clientOnly) return null
|
|
59
|
+
if (attr.value.kind !== 'expression') {
|
|
60
|
+
return { kind: 'dynamic', expr: '', loc: attr.loc }
|
|
61
|
+
}
|
|
62
|
+
const parsed = attr.value.parsed ?? parseExpression(attr.value.expr.trim())
|
|
63
|
+
const html = staticHtmlLiteral(parsed)
|
|
64
|
+
if (html !== null) return { kind: 'static', html }
|
|
65
|
+
return { kind: 'dynamic', expr: attr.value.expr, loc: attr.loc }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* `{ __html: '<b>bold</b>' }` → the literal string, nothing else. Exactly
|
|
70
|
+
* one property, key `__html` (identifier or string form — `{ __html: … }`
|
|
71
|
+
* and `{ '__html': … }` are equivalent JS), non-shorthand (`{ __html }`
|
|
72
|
+
* would read a variable, not a literal), value a compile-time STRING
|
|
73
|
+
* LITERAL `ParsedExpr` (`{kind:'literal', literalType:'string'}`) — which
|
|
74
|
+
* a no-substitution template literal (`` `<b>bold</b>` ``) already parses
|
|
75
|
+
* to, so it's accepted the same as a quoted string. A template literal
|
|
76
|
+
* WITH a substitution, a local `const`, string concatenation, or any other
|
|
77
|
+
* non-literal shape all fall through to `null` — no const-folding, no
|
|
78
|
+
* partial evaluation. Widening this is a single-place change here,
|
|
79
|
+
* deliberately not attempted in v1 (#2207).
|
|
80
|
+
*/
|
|
81
|
+
function staticHtmlLiteral(parsed: ParsedExpr): string | null {
|
|
82
|
+
if (parsed.kind !== 'object-literal') return null
|
|
83
|
+
if (parsed.properties.length !== 1) return null
|
|
84
|
+
const [prop] = parsed.properties
|
|
85
|
+
if (prop.shorthand) return null
|
|
86
|
+
if (prop.key !== '__html') return null
|
|
87
|
+
if (prop.value.kind !== 'literal' || prop.value.literalType !== 'string') return null
|
|
88
|
+
return prop.value.value as string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Per-adapter template-metacharacter guard for the STATIC-literal path.
|
|
93
|
+
* The literal is spliced into the adapter's own template source as plain
|
|
94
|
+
* text — safe against HTML injection (it's the developer's own compile-time
|
|
95
|
+
* string, same trust boundary as writing the HTML by hand), but NOT
|
|
96
|
+
* automatically safe against TEMPLATE-source injection: a literal
|
|
97
|
+
* containing `{{ … }}` (Go/Jinja/minijinja/Twig), `<% … %>` (ERB/
|
|
98
|
+
* Mojolicious), `{!! !!}`/`@directive`/`<?php`/`<x-…>` (Blade — the last is
|
|
99
|
+
* Laravel's component-tag syntax, on by default; see below), or `<: … :>`
|
|
100
|
+
* (Xslate) would be interpreted as a live template construct instead of
|
|
101
|
+
* inert text once spliced in. Mojolicious and Xslate also support
|
|
102
|
+
* whole-line "line code" (a line starting with `%`/`:`, possibly indented —
|
|
103
|
+
* `\s` covers the same whitespace class Mojo::Template/Xslate strip),
|
|
104
|
+
* checked with `m`. Refuses (via {@link dangerousInnerHtmlDiagnostic})
|
|
105
|
+
* rather than escaping — escaping a supposedly-raw-HTML literal would
|
|
106
|
+
* silently corrupt the developer's own markup, which is worse than a loud
|
|
107
|
+
* compile-time refusal.
|
|
108
|
+
*
|
|
109
|
+
* Blade's `<x-…>`/`</x-…>`: this project's `BladeBackend` constructs a
|
|
110
|
+
* plain `Illuminate\View\Compilers\BladeCompiler`, whose component-tag
|
|
111
|
+
* compiler (`$compilesComponentTags = true` by default) resolves and
|
|
112
|
+
* renders a live Blade component for `<x-foo>` — NOT template-substitution,
|
|
113
|
+
* full component-class/view resolution (Fable review, #2217). This is the
|
|
114
|
+
* single sharpest case of "not inert text once spliced" in the whole guard
|
|
115
|
+
* table, sharper than a bare `{{ }}` interpolation.
|
|
116
|
+
*
|
|
117
|
+
* Go's `html/template` context-aware escaper is a DOCUMENTED caveat, not a
|
|
118
|
+
* guarded case: it parses the spliced literal to pick escaping contexts for
|
|
119
|
+
* SUBSEQUENT `{{ }}` actions in the same template, so malformed markup (an
|
|
120
|
+
* unclosed `<script>`/`<style>`) can produce a template `Parse` error or
|
|
121
|
+
* mis-contexted escaping of later actions — output corruption, never
|
|
122
|
+
* injection, and no different from hand-writing the same malformed HTML
|
|
123
|
+
* into the template (this module's stated trust domain). The `{{` guard
|
|
124
|
+
* below still catches the actual injection vector (a literal containing a
|
|
125
|
+
* live action).
|
|
126
|
+
*/
|
|
127
|
+
const TEMPLATE_METACHAR_PATTERNS: Readonly<Record<string, RegExp>> = {
|
|
128
|
+
blade: /\{\{|\{!!|<\?|@\w|<\/?\s*x[-:]/,
|
|
129
|
+
erb: /<%/,
|
|
130
|
+
'go-template': /\{\{/,
|
|
131
|
+
jinja: /\{\{|\{%|\{#/,
|
|
132
|
+
minijinja: /\{\{|\{%|\{#/,
|
|
133
|
+
mojolicious: /<%|^\s*%/m,
|
|
134
|
+
twig: /\{\{|\{%|\{#/,
|
|
135
|
+
xslate: /<:|^\s*:/m,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* `null` when `html` is safe to splice as-is into `adapterId`'s template
|
|
140
|
+
* source; otherwise a human-readable reason naming the offending adapter's
|
|
141
|
+
* metacharacter family (or the missing-guard case below), for the refusal
|
|
142
|
+
* message.
|
|
143
|
+
*
|
|
144
|
+
* FAILS CLOSED for an `adapterId` with no entry in
|
|
145
|
+
* {@link TEMPLATE_METACHAR_PATTERNS} (Fable review, #2217): a 9th template
|
|
146
|
+
* adapter added later — e.g. by following the repo's own `add-adapter`
|
|
147
|
+
* playbook, which will tell it to wire the same
|
|
148
|
+
* `resolveDangerousInnerHtml`/`dangerousInnerHtmlMetacharViolation` calls
|
|
149
|
+
* every existing adapter does — must not silently get an unguarded splice
|
|
150
|
+
* just because nobody remembered to extend this table. Refusing loudly is
|
|
151
|
+
* the safe default; only the 8 adapters actually verified against their
|
|
152
|
+
* own template compiler's syntax get to skip the refusal.
|
|
153
|
+
*/
|
|
154
|
+
export function dangerousInnerHtmlMetacharViolation(html: string, adapterId: string): string | null {
|
|
155
|
+
const pattern = TEMPLATE_METACHAR_PATTERNS[adapterId]
|
|
156
|
+
if (!pattern) {
|
|
157
|
+
return `no template-metacharacter guard is defined for adapter '${adapterId}' — refusing rather than splicing unguarded`
|
|
158
|
+
}
|
|
159
|
+
if (!pattern.test(html)) return null
|
|
160
|
+
return `the literal HTML contains a sequence ${adapterId}'s own template compiler would interpret (not inert text once spliced into the template)`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Purpose-built `BF101` for a `dangerouslySetInnerHTML` value this adapter
|
|
165
|
+
* can't lower — either genuinely dynamic (not a literal) or a static
|
|
166
|
+
* literal that fails the metachar guard above. Named `reason` so both
|
|
167
|
+
* refusal paths funnel through the same message shape rather than growing
|
|
168
|
+
* two near-identical adapter-side error strings.
|
|
169
|
+
*/
|
|
170
|
+
export function dangerousInnerHtmlDiagnostic(
|
|
171
|
+
expr: string,
|
|
172
|
+
loc: SourceLocation,
|
|
173
|
+
reason?: string,
|
|
174
|
+
): CompilerError {
|
|
175
|
+
const detail = reason ? ` — ${reason}.` : ''
|
|
176
|
+
return {
|
|
177
|
+
code: 'BF101',
|
|
178
|
+
severity: 'error',
|
|
179
|
+
message: `dangerouslySetInnerHTML requires a compile-time string literal __html value on template adapters (e.g. { __html: '...' })${expr ? `: ${expr.trim()}` : ''}${detail}`,
|
|
180
|
+
loc,
|
|
181
|
+
suggestion: {
|
|
182
|
+
message:
|
|
183
|
+
'Dynamic or signal-derived HTML for dangerouslySetInnerHTML is only supported on Hono/CSR today (tracked separately: https://github.com/piconic-ai/barefootjs/issues/2215). Use an inline string literal, or defer it to the client with /* @client */ (e.g. dangerouslySetInnerHTML={/* @client */ { __html: expr }}) so hydration sets it instead of SSR.',
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every name a `.map()`/`.filter()` loop callback binds as its item or
|
|
3
|
+
* index parameter, anywhere in a component's IR tree (#2212 review
|
|
4
|
+
* finding). `isStringTypedOperand`'s identifier arm resolves a bare
|
|
5
|
+
* identifier against `collectStringValueNames`'s flat, scope-BLIND
|
|
6
|
+
* `Set<string>` — so a loop callback whose param happens to reuse a
|
|
7
|
+
* string prop's name (`items.map((name) => ... 1 + name ...)`, where the
|
|
8
|
+
* component also has a string `name` prop) would otherwise be
|
|
9
|
+
* misdetected as string-typed, silently lowering a NUMERIC `+` to the
|
|
10
|
+
* adapter's string-concat operator instead of leaving it numeric — wrong
|
|
11
|
+
* rendered output, not a refusal. Subtracting every loop-bound name from
|
|
12
|
+
* the string-name set (in each adapter's `collectStringValueNames`) is a
|
|
13
|
+
* coarse but SAFE mitigation: a name used as a loop param anywhere in the
|
|
14
|
+
* component never gets string-concat treatment anywhere in the
|
|
15
|
+
* component, even at a non-shadowed use site outside that loop — which
|
|
16
|
+
* degrades to the ALREADY-accepted residual (`+` falls back to numeric,
|
|
17
|
+
* same as before #2212 for an unresolvable operand) rather than ever
|
|
18
|
+
* producing silently-wrong output.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ComponentIR, IRNode } from '../types.ts'
|
|
22
|
+
|
|
23
|
+
export function collectLoopBoundNames(ir: ComponentIR): Set<string> {
|
|
24
|
+
const names = new Set<string>()
|
|
25
|
+
const visit = (node: IRNode | null | undefined): void => {
|
|
26
|
+
if (!node) return
|
|
27
|
+
switch (node.type) {
|
|
28
|
+
case 'element':
|
|
29
|
+
case 'component':
|
|
30
|
+
case 'fragment':
|
|
31
|
+
case 'provider':
|
|
32
|
+
for (const child of node.children) visit(child)
|
|
33
|
+
break
|
|
34
|
+
case 'async':
|
|
35
|
+
visit(node.fallback)
|
|
36
|
+
for (const child of node.children) visit(child)
|
|
37
|
+
break
|
|
38
|
+
case 'loop':
|
|
39
|
+
names.add(node.param)
|
|
40
|
+
if (node.index) names.add(node.index)
|
|
41
|
+
// A destructured callback param (`.map(({ name }) => ...)`) binds
|
|
42
|
+
// its extracted names via `paramBindings`, not `param` itself
|
|
43
|
+
// (`param` holds the raw pattern text there) — adapters that lower
|
|
44
|
+
// the destructure to a `{% set name = __bf_item.name %}`-style
|
|
45
|
+
// local (#2087) leave `name` reachable as a bare identifier in the
|
|
46
|
+
// body, so it needs the same exclusion as a plain loop param.
|
|
47
|
+
for (const binding of node.paramBindings ?? []) names.add(binding.name)
|
|
48
|
+
// A `.filter(pred).map(cb)` chain's filter predicate is emitted
|
|
49
|
+
// through the same binary/string-name machinery, using its OWN
|
|
50
|
+
// param (which may differ from the map callback's `param`) before
|
|
51
|
+
// any rename to the loop param happens.
|
|
52
|
+
if (node.filterPredicate) names.add(node.filterPredicate.param)
|
|
53
|
+
for (const child of node.children) visit(child)
|
|
54
|
+
if (node.childComponent) {
|
|
55
|
+
for (const child of node.childComponent.children) visit(child)
|
|
56
|
+
}
|
|
57
|
+
for (const nested of node.nestedComponents ?? []) {
|
|
58
|
+
for (const child of nested.children) visit(child)
|
|
59
|
+
}
|
|
60
|
+
for (const frag of node.flatMapCallback?.fragments ?? []) {
|
|
61
|
+
visit(frag.ir)
|
|
62
|
+
}
|
|
63
|
+
break
|
|
64
|
+
case 'conditional':
|
|
65
|
+
visit(node.whenTrue)
|
|
66
|
+
visit(node.whenFalse)
|
|
67
|
+
break
|
|
68
|
+
case 'if-statement':
|
|
69
|
+
visit(node.consequent)
|
|
70
|
+
if (node.alternate) visit(node.alternate)
|
|
71
|
+
break
|
|
72
|
+
case 'text':
|
|
73
|
+
case 'expression':
|
|
74
|
+
case 'slot':
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
visit(ir.root)
|
|
79
|
+
return names
|
|
80
|
+
}
|
|
@@ -67,11 +67,14 @@ export type ArrayMethod =
|
|
|
67
67
|
| 'toLowerCase'
|
|
68
68
|
| 'toUpperCase'
|
|
69
69
|
| 'trim'
|
|
70
|
+
| 'trimStart'
|
|
71
|
+
| 'trimEnd'
|
|
70
72
|
| 'toFixed'
|
|
71
73
|
| 'split'
|
|
72
74
|
| 'startsWith'
|
|
73
75
|
| 'endsWith'
|
|
74
76
|
| 'replace'
|
|
77
|
+
| 'replaceAll'
|
|
75
78
|
| 'repeat'
|
|
76
79
|
| 'padStart'
|
|
77
80
|
| 'padEnd'
|
|
@@ -111,10 +114,16 @@ export interface ParsedExprEmitter {
|
|
|
111
114
|
identifier(name: string): string
|
|
112
115
|
literal(value: string | number | boolean | null, literalType: LiteralType): string
|
|
113
116
|
call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string
|
|
117
|
+
// `optional` is true for a `?.`-written access (`user?.name`); see the
|
|
118
|
+
// `ParsedExpr` `member` variant's docstring in `expression-parser.ts`
|
|
119
|
+
// for the single-hop caveat. Every adapter's `member()` implementation
|
|
120
|
+
// that doesn't need it (its lowering is already null-safe) is free to
|
|
121
|
+
// ignore the parameter.
|
|
114
122
|
member(
|
|
115
123
|
object: ParsedExpr,
|
|
116
124
|
property: string,
|
|
117
125
|
computed: boolean,
|
|
126
|
+
optional: boolean,
|
|
118
127
|
emit: (e: ParsedExpr) => string,
|
|
119
128
|
): string
|
|
120
129
|
// Element access with a non-literal index (`arr[index]`). The index
|
|
@@ -219,6 +228,15 @@ export function isStringTypedOperand(expr: ParsedExpr, isStringName: (n: string)
|
|
|
219
228
|
if (expr.kind === 'member' && expr.object.kind === 'identifier' && expr.object.name === 'props') {
|
|
220
229
|
return isStringName(expr.property)
|
|
221
230
|
}
|
|
231
|
+
// A bare identifier (#2212): a destructured prop param or a same-file
|
|
232
|
+
// local const, string-typed per the caller's `isStringName` set — each
|
|
233
|
+
// adapter's `collectStringValueNames` already tracks `propsParams`, so a
|
|
234
|
+
// component's own `{ a, b }: { a: string; b: string }` destructure was
|
|
235
|
+
// only unreachable here for lack of this arm; extending
|
|
236
|
+
// `collectStringValueNames` to also walk `ir.metadata.localConstants`
|
|
237
|
+
// (adapter-side change) closes the "two same-file string consts" shape
|
|
238
|
+
// the same way.
|
|
239
|
+
if (expr.kind === 'identifier') return isStringName(expr.name)
|
|
222
240
|
if (expr.kind === 'binary' && expr.op === '+') {
|
|
223
241
|
return isStringTypedOperand(expr.left, isStringName) || isStringTypedOperand(expr.right, isStringName)
|
|
224
242
|
}
|
|
@@ -288,7 +306,7 @@ export function emitParsedExpr(expr: ParsedExpr, emitter: ParsedExprEmitter): st
|
|
|
288
306
|
return emitter.call(expr.callee, expr.args, emit)
|
|
289
307
|
}
|
|
290
308
|
case 'member':
|
|
291
|
-
return emitter.member(expr.object, expr.property, expr.computed, emit)
|
|
309
|
+
return emitter.member(expr.object, expr.property, expr.computed, expr.optional, emit)
|
|
292
310
|
case 'index-access':
|
|
293
311
|
return emitter.indexAccess(expr.object, expr.index, emit)
|
|
294
312
|
case 'binary':
|
package/src/analyzer.ts
CHANGED
|
@@ -50,6 +50,11 @@ export function needsTypeBasedDetection(source: string): boolean {
|
|
|
50
50
|
if (REACTIVE_BRAND_PACKAGES.some(pkg => source.includes(pkg))) return true
|
|
51
51
|
// BF023/BF024 nullable-key check needs getTypeAtLocation() on the key expression.
|
|
52
52
|
if (/\.map\s*\(/.test(source)) return true
|
|
53
|
+
// createSelector's returned accessor is Reactive<>-branded like a library
|
|
54
|
+
// accessor above — a selector call outside any `.map()` (no loop in the
|
|
55
|
+
// file at all) would otherwise skip the TypeChecker entirely and miss the
|
|
56
|
+
// brand.
|
|
57
|
+
if (source.includes('createSelector')) return true
|
|
53
58
|
return false
|
|
54
59
|
}
|
|
55
60
|
|
|
@@ -1828,6 +1833,7 @@ function collectLocalDeclarations(root: ts.Node): Set<string> {
|
|
|
1828
1833
|
// and are emitted by the compiler for 'use client' components.
|
|
1829
1834
|
const CLIENT_EXPORTS = new Set([
|
|
1830
1835
|
'createSignal', 'createEffect', 'createDisposableEffect', 'createMemo',
|
|
1836
|
+
'createSelector',
|
|
1831
1837
|
'createRoot', 'onCleanup', 'onMount', 'untrack', 'batch', 'splitProps',
|
|
1832
1838
|
'forwardProps', 'unwrap', '__slot',
|
|
1833
1839
|
'createContext', 'useContext', 'provideContext',
|
package/src/expression-parser.ts
CHANGED
|
@@ -25,7 +25,21 @@ export type ParsedExpr =
|
|
|
25
25
|
// canonical form in `value`.
|
|
26
26
|
| { kind: 'literal'; value: string | number | boolean | null; literalType: 'string' | 'number' | 'boolean' | 'null'; raw?: string }
|
|
27
27
|
| { kind: 'call'; callee: ParsedExpr; args: ParsedExpr[] }
|
|
28
|
-
|
|
28
|
+
// `optional` is true for a `?.`-written access (`user?.name`,
|
|
29
|
+
// `arr?.[0]`) — the ONE hop that was actually optional-chained in JS
|
|
30
|
+
// source; a plain `.`/`[]` access is `false`. Adapters whose member
|
|
31
|
+
// lowering is already null-safe by construction (e.g. Jinja's `[]`
|
|
32
|
+
// subscript swallows a `None` receiver, Blade's `data_get` helper)
|
|
33
|
+
// ignore this field entirely; Go and ERB (whose native `.` access
|
|
34
|
+
// panics/raises on a nil/undefined receiver) route an `optional: true`
|
|
35
|
+
// access through a defensive form (`bf_get`, Ruby `&.`) instead. NOTE:
|
|
36
|
+
// this does NOT reproduce JS's whole-chain short-circuit for a
|
|
37
|
+
// MULTI-hop chain (`a?.b.c` also guards `.c` in JS once `a` is
|
|
38
|
+
// nullish) — only the single hop written with `?.` is marked; a
|
|
39
|
+
// following plain `.c` on a possibly-nullish `a?.b` is a known,
|
|
40
|
+
// untracked limitation on Go/ERB (not exercised by the current
|
|
41
|
+
// `optional-chaining-prop` fixture, which is single-hop only).
|
|
42
|
+
| { kind: 'member'; object: ParsedExpr; property: string; computed: boolean; optional: boolean }
|
|
29
43
|
// Element access with a NON-literal index (`selected()[index]`,
|
|
30
44
|
// `rows[i + 1]`). A literal-index access (`arr[0]`, `obj['key']`)
|
|
31
45
|
// stays a `member` (computed) since the key is statically known and
|
|
@@ -88,11 +102,14 @@ export type ParsedExpr =
|
|
|
88
102
|
| 'toLowerCase'
|
|
89
103
|
| 'toUpperCase'
|
|
90
104
|
| 'trim'
|
|
105
|
+
| 'trimStart'
|
|
106
|
+
| 'trimEnd'
|
|
91
107
|
| 'toFixed'
|
|
92
108
|
| 'split'
|
|
93
109
|
| 'startsWith'
|
|
94
110
|
| 'endsWith'
|
|
95
111
|
| 'replace'
|
|
112
|
+
| 'replaceAll'
|
|
96
113
|
| 'repeat'
|
|
97
114
|
| 'padStart'
|
|
98
115
|
| 'padEnd'
|
|
@@ -345,18 +362,17 @@ const UNSUPPORTED_METHODS = new Set([
|
|
|
345
362
|
// `startsWith` / `endsWith` are no longer here — both lower via the
|
|
346
363
|
// `array-method` IR + `bf_starts_with` / `bf_ends_with` (Go) and
|
|
347
364
|
// `bf->starts_with` / `bf->ends_with` (Mojo). See #1448 Tier B.
|
|
348
|
-
// `replace`
|
|
349
|
-
// the `array-method` IR + `bf_replace`
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
365
|
+
// `replace` / `replaceAll` are no longer here — the string-pattern
|
|
366
|
+
// form of each lowers via the `array-method` IR + `bf_replace` /
|
|
367
|
+
// `bf_replace_all` (Go) / `bf->replace` / `bf->replace_all` (Mojo);
|
|
368
|
+
// the regex-pattern form of EITHER is refused at the parse arm below
|
|
369
|
+
// (it would need the per-adapter regex-flavour decision).
|
|
353
370
|
// `repeat` is no longer here — `String.prototype.repeat(n)` lowers via
|
|
354
371
|
// the `array-method` IR + `bf_repeat` (Go) / `bf->repeat` (Mojo).
|
|
355
372
|
// See #1448 Tier B.
|
|
356
373
|
// `padStart` / `padEnd` are no longer here — both lower via the
|
|
357
374
|
// `array-method` IR + `bf_pad_start` / `bf_pad_end` (Go) and
|
|
358
375
|
// `bf->pad_start` / `bf->pad_end` (Mojo). See #1448 Tier B.
|
|
359
|
-
'replaceAll',
|
|
360
376
|
'charAt', 'charCodeAt', 'codePointAt', 'normalize',
|
|
361
377
|
'substring', 'substr', 'match', 'matchAll', 'search',
|
|
362
378
|
])
|
|
@@ -882,6 +898,16 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
882
898
|
if (callee.property === 'trim') {
|
|
883
899
|
return { kind: 'array-method', method: 'trim', object: callee.object, args }
|
|
884
900
|
}
|
|
901
|
+
// `.trimStart()` / `.trimEnd()` — the one-sided siblings of
|
|
902
|
+
// `.trim()` (#2183 follow-up). No array equivalent exists, so
|
|
903
|
+
// there's no receiver-type ambiguity to resolve (unlike `.slice`);
|
|
904
|
+
// each is a dedicated `array-method` variant (matching the
|
|
905
|
+
// `padStart`/`padEnd` precedent — separate members, not a shared
|
|
906
|
+
// member with a `side` flag) lowering to its own runtime helper
|
|
907
|
+
// per adapter, not `.trim()` with a flag.
|
|
908
|
+
if (callee.property === 'trimStart' || callee.property === 'trimEnd') {
|
|
909
|
+
return { kind: 'array-method', method: callee.property, object: callee.object, args }
|
|
910
|
+
}
|
|
885
911
|
// `.toFixed(digits?)` — Number → fixed-decimal string. The digit
|
|
886
912
|
// count (default 0) travels as the single arg; all adapters route
|
|
887
913
|
// through a `to_fixed` runtime helper (Perl) / `fmt.Sprintf` (Go)
|
|
@@ -954,19 +980,25 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
954
980
|
// refused: `isSupported` maps a regex-pattern `.replace` to the
|
|
955
981
|
// deferred-form BF101 reason — the Perl `s///` vs Go
|
|
956
982
|
// `regexp.ReplaceAllString` flavour gap is the open design
|
|
957
|
-
// question in #1448.
|
|
983
|
+
// question in #1448. `.replaceAll(pattern, replacement)` shares
|
|
984
|
+
// this arm (#2182 follow-up): same arity/regex/object-literal
|
|
985
|
+
// gates, only the `method` tag and its runtime helper differ —
|
|
986
|
+
// `bf_replace_all` (Go, `strings.ReplaceAll`) / `bf->replace_all`
|
|
987
|
+
// (Mojo) replace EVERY occurrence, where `.replace`'s helpers
|
|
988
|
+
// replace only the first.
|
|
958
989
|
//
|
|
959
990
|
// Full JS arity: a third+ argument is ignored (the adapter reads
|
|
960
991
|
// only the pattern + replacement). The one- and zero-argument
|
|
961
992
|
// forms are refused: JS coerces the missing replacement (and
|
|
962
993
|
// pattern) to the literal string "undefined", a degenerate result
|
|
963
994
|
// (mirrors the `.includes()` / `.startsWith()` zero-arg refusal).
|
|
964
|
-
if (callee.property === 'replace') {
|
|
995
|
+
if (callee.property === 'replace' || callee.property === 'replaceAll') {
|
|
996
|
+
const method = callee.property
|
|
965
997
|
if (args.length < 2) {
|
|
966
998
|
return {
|
|
967
999
|
kind: 'unsupported',
|
|
968
1000
|
raw,
|
|
969
|
-
reason:
|
|
1001
|
+
reason: `\`.${method}(${args.length === 0 ? '' : 'pattern'})\` needs both a pattern and a replacement — JS coerces the missing argument to the string "undefined", a degenerate result. Pass both arguments, or pre-compute the value before the template.`,
|
|
970
1002
|
}
|
|
971
1003
|
}
|
|
972
1004
|
// A regex-literal pattern is the deferred form (the Perl `s///`
|
|
@@ -980,7 +1012,7 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
980
1012
|
// any template use with the deferred-form reason.
|
|
981
1013
|
const patternNode = node.arguments[0]
|
|
982
1014
|
if (patternNode && ts.isRegularExpressionLiteral(patternNode)) {
|
|
983
|
-
return { kind: 'array-method', method
|
|
1015
|
+
return { kind: 'array-method', method, object: callee.object, args }
|
|
984
1016
|
}
|
|
985
1017
|
// Treat an object-literal argument like `unsupported` — a `.replace`
|
|
986
1018
|
// with an object pattern/replacement isn't lowerable, same as before
|
|
@@ -995,7 +1027,7 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
995
1027
|
const reason = badArg.kind === 'unsupported' ? badArg.reason : 'Unsupported syntax: ObjectLiteralExpression'
|
|
996
1028
|
return { kind: 'unsupported', raw, reason }
|
|
997
1029
|
}
|
|
998
|
-
return { kind: 'array-method', method
|
|
1030
|
+
return { kind: 'array-method', method, object: callee.object, args }
|
|
999
1031
|
}
|
|
1000
1032
|
// `.repeat(n)` — string → string (the receiver concatenated `n`
|
|
1001
1033
|
// times). Go uses `bf_repeat` (`strings.Repeat`, clamping a
|
|
@@ -1072,7 +1104,7 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
1072
1104
|
const property = node.name.text
|
|
1073
1105
|
|
|
1074
1106
|
// Return as normal member - filter.length is handled in adapter
|
|
1075
|
-
return { kind: 'member', object, property, computed: false }
|
|
1107
|
+
return { kind: 'member', object, property, computed: false, optional: !!node.questionDotToken }
|
|
1076
1108
|
}
|
|
1077
1109
|
|
|
1078
1110
|
// Element access: items[0], obj['key']
|
|
@@ -1088,10 +1120,10 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
1088
1120
|
}
|
|
1089
1121
|
// For simple number/string access, store as property
|
|
1090
1122
|
if (ts.isNumericLiteral(argNode)) {
|
|
1091
|
-
return { kind: 'member', object, property: argNode.text, computed: true }
|
|
1123
|
+
return { kind: 'member', object, property: argNode.text, computed: true, optional: !!node.questionDotToken }
|
|
1092
1124
|
}
|
|
1093
1125
|
if (ts.isStringLiteral(argNode)) {
|
|
1094
|
-
return { kind: 'member', object, property: argNode.text, computed: true }
|
|
1126
|
+
return { kind: 'member', object, property: argNode.text, computed: true, optional: !!node.questionDotToken }
|
|
1095
1127
|
}
|
|
1096
1128
|
// Variable / expression index (`selected()[index]`, `rows[i + 1]`):
|
|
1097
1129
|
// carry the index as its own ParsedExpr so the adapter can lower it
|
|
@@ -2063,7 +2095,7 @@ function substituteDestructuredFields(
|
|
|
2063
2095
|
// one-hop chain identical to the pre-#1530 shape.
|
|
2064
2096
|
let node: ParsedExpr = { kind: 'identifier', name: syntheticParam }
|
|
2065
2097
|
for (const segment of entry.path) {
|
|
2066
|
-
node = { kind: 'member', object: node, property: segment, computed: false }
|
|
2098
|
+
node = { kind: 'member', object: node, property: segment, computed: false, optional: false }
|
|
2067
2099
|
}
|
|
2068
2100
|
// Default value (#1531): wrap the accessor in `?? <default>` so
|
|
2069
2101
|
// a missing field falls back to the user-supplied literal /
|
|
@@ -2088,9 +2120,10 @@ function substituteDestructuredFields(
|
|
|
2088
2120
|
object: { kind: 'identifier', name: syntheticParam },
|
|
2089
2121
|
property: e.property,
|
|
2090
2122
|
computed: false,
|
|
2123
|
+
optional: e.optional,
|
|
2091
2124
|
}
|
|
2092
2125
|
}
|
|
2093
|
-
return { kind: 'member', object: walk(e.object), property: e.property, computed: e.computed }
|
|
2126
|
+
return { kind: 'member', object: walk(e.object), property: e.property, computed: e.computed, optional: e.optional }
|
|
2094
2127
|
case 'index-access':
|
|
2095
2128
|
return { kind: 'index-access', object: walk(e.object), index: walk(e.index) }
|
|
2096
2129
|
case 'binary':
|
|
@@ -2236,15 +2269,16 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2236
2269
|
}
|
|
2237
2270
|
|
|
2238
2271
|
case 'array-method': {
|
|
2239
|
-
// A regex-pattern `.replace` is carried structurally
|
|
2240
|
-
// arg) but is the deferred form (#1448) — no
|
|
2241
|
-
// Refuse with the dedicated reason
|
|
2242
|
-
//
|
|
2243
|
-
|
|
2272
|
+
// A regex-pattern `.replace` / `.replaceAll` is carried structurally
|
|
2273
|
+
// (a `regex` first arg) but is the deferred form (#1448) — no
|
|
2274
|
+
// template language lowers it. Refuse with the dedicated reason
|
|
2275
|
+
// rather than the generic standalone-regex message, preserving the
|
|
2276
|
+
// diagnostic the parser used to emit directly.
|
|
2277
|
+
if ((expr.method === 'replace' || expr.method === 'replaceAll') && expr.args[0]?.kind === 'regex') {
|
|
2244
2278
|
return {
|
|
2245
2279
|
supported: false,
|
|
2246
2280
|
reason:
|
|
2247
|
-
|
|
2281
|
+
`String.prototype.${expr.method} supports only a string pattern + string replacement (the regex form is deferred); use a string pattern or wrap the expression in /* @client */`,
|
|
2248
2282
|
}
|
|
2249
2283
|
}
|
|
2250
2284
|
const objSupport = checkSupport(expr.object)
|
|
@@ -2893,7 +2927,7 @@ function inlineBinding(
|
|
|
2893
2927
|
case 'call':
|
|
2894
2928
|
return { kind: 'call', callee: walk(e.callee, enclosing), args: e.args.map(a => walk(a, enclosing)) }
|
|
2895
2929
|
case 'member':
|
|
2896
|
-
return { kind: 'member', object: walk(e.object, enclosing), property: e.property, computed: e.computed }
|
|
2930
|
+
return { kind: 'member', object: walk(e.object, enclosing), property: e.property, computed: e.computed, optional: e.optional }
|
|
2897
2931
|
case 'index-access':
|
|
2898
2932
|
return { kind: 'index-access', object: walk(e.object, enclosing), index: walk(e.index, enclosing) }
|
|
2899
2933
|
case 'binary':
|
|
@@ -3281,7 +3315,7 @@ export function materializeGetterCalls(expr: ParsedExpr, names: ReadonlySet<stri
|
|
|
3281
3315
|
alternate: rw(expr.alternate),
|
|
3282
3316
|
}
|
|
3283
3317
|
case 'member':
|
|
3284
|
-
return { kind: 'member', object: rw(expr.object), property: expr.property, computed: expr.computed }
|
|
3318
|
+
return { kind: 'member', object: rw(expr.object), property: expr.property, computed: expr.computed, optional: expr.optional }
|
|
3285
3319
|
case 'index-access':
|
|
3286
3320
|
return { kind: 'index-access', object: rw(expr.object), index: rw(expr.index) }
|
|
3287
3321
|
case 'template-literal':
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ export type {
|
|
|
49
49
|
ParamInfo,
|
|
50
50
|
PropertyInfo,
|
|
51
51
|
MemoInfo,
|
|
52
|
+
ConstantInfo,
|
|
52
53
|
TypeInfo,
|
|
53
54
|
TypeDefinition,
|
|
54
55
|
SourceLocation,
|
|
@@ -87,6 +88,9 @@ export type { JsxAdapterConfig } from './adapters/jsx-adapter.ts'
|
|
|
87
88
|
export { rewriteImportsForTemplate } from './adapters/template-imports.ts'
|
|
88
89
|
export { emitParsedExpr, groupBinaryOperand, isStringTypedOperand, isStringConcatBinary } from './adapters/parsed-expr-emitter.ts'
|
|
89
90
|
export type { ParsedExprEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
|
|
91
|
+
export { collectLoopBoundNames } from './adapters/loop-bound-names.ts'
|
|
92
|
+
export { evaluateSignalInit, tryEvaluateSignalInit, type SignalInitEvalResult } from './signal-init-eval.ts'
|
|
93
|
+
export { evaluateStaticLiteral, isFullyStaticLiteral, resolveStaticLoopSource } from './static-literal.ts'
|
|
90
94
|
export { importsSearchParams, searchParamsLocalNames, envSignalLocalNames, envSignalReaderFor, ENV_SIGNAL_READERS, queryHrefLocalNames, matchSearchParamsMethodCall } from './adapters/env-signal.ts'
|
|
91
95
|
export type { EnvSignalReader } from './adapters/env-signal.ts'
|
|
92
96
|
export { matchQueryHrefCall, queryHrefArgs, type QueryHrefCall, type QueryHrefTriple } from './query-href-lowering.ts'
|
|
@@ -116,6 +120,13 @@ export { emitIRNode } from './adapters/ir-node-emitter.ts'
|
|
|
116
120
|
export type { IRNodeEmitter, EmitIRNode } from './adapters/ir-node-emitter.ts'
|
|
117
121
|
export { emitAttrValue } from './adapters/attr-value-emitter.ts'
|
|
118
122
|
export type { AttrValueEmitter } from './adapters/attr-value-emitter.ts'
|
|
123
|
+
export {
|
|
124
|
+
isDangerousInnerHtmlAttr,
|
|
125
|
+
resolveDangerousInnerHtml,
|
|
126
|
+
dangerousInnerHtmlMetacharViolation,
|
|
127
|
+
dangerousInnerHtmlDiagnostic,
|
|
128
|
+
} from './adapters/dangerous-inner-html.ts'
|
|
129
|
+
export type { DangerousInnerHtmlResolution } from './adapters/dangerous-inner-html.ts'
|
|
119
130
|
|
|
120
131
|
// Client JS Generator
|
|
121
132
|
export { generateClientJs, generateClientJsWithSourceMap, analyzeClientNeeds } from './ir-to-client-js/index.ts'
|
|
@@ -272,15 +283,20 @@ export interface BuildOptions {
|
|
|
272
283
|
* - `'treeshake'` (default) — bundle only the runtime exports this
|
|
273
284
|
* project's compiled client JS actually imports, plus a small
|
|
274
285
|
* always-kept public mount API (`render`, `hydrate`, etc.).
|
|
286
|
+
* - `'treeshake-exact'` — same collection, but without the always-kept
|
|
287
|
+
* set. Smaller output; a hand-written page script the CLI never
|
|
288
|
+
* compiles must list any runtime names it calls directly in
|
|
289
|
+
* `runtimeKeep`, or they're silently dropped.
|
|
275
290
|
* - `'full'` — copy the entire prebuilt runtime bundle verbatim.
|
|
276
291
|
* See `@barefootjs/cli`'s `runtime-treeshake.ts` for the collector and
|
|
277
292
|
* `ALWAYS_KEEP_RUNTIME_EXPORTS` for the always-kept names.
|
|
278
293
|
*/
|
|
279
|
-
runtimeBundle?: 'treeshake' | 'full'
|
|
294
|
+
runtimeBundle?: 'treeshake' | 'treeshake-exact' | 'full'
|
|
280
295
|
/**
|
|
281
296
|
* Extra `@barefootjs/client*` export names to force-keep in `barefoot.js`
|
|
282
|
-
* under `runtimeBundle: 'treeshake'` — for names
|
|
283
|
-
* from hand-written page scripts the CLI never
|
|
297
|
+
* under `runtimeBundle: 'treeshake'` or `'treeshake-exact'` — for names
|
|
298
|
+
* only ever referenced from hand-written page scripts the CLI never
|
|
299
|
+
* compiles.
|
|
284
300
|
*/
|
|
285
301
|
runtimeKeep?: string[]
|
|
286
302
|
}
|