@barefootjs/go-template 0.5.3 → 0.6.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/adapter/go-template-adapter.d.ts +5 -2
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +104 -23
- package/dist/build.js +104 -23
- package/dist/index.js +104 -23
- package/package.json +2 -2
- package/src/__tests__/go-template-adapter.test.ts +199 -18
- package/src/adapter/go-template-adapter.ts +213 -22
- package/src/test-render.ts +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/go-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Go html/template adapter for BarefootJS - generates Go template files from IR",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -54,6 +54,6 @@
|
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
57
|
-
"@barefootjs/jsx": "0.
|
|
57
|
+
"@barefootjs/jsx": "0.6.1"
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -2182,11 +2182,26 @@ import { fixture as arrayLastIndexOfFixture } from '../../../adapter-tests/fixtu
|
|
|
2182
2182
|
import { fixture as arrayAtFixture } from '../../../adapter-tests/fixtures/methods/array-at'
|
|
2183
2183
|
import { fixture as arrayConcatFixture } from '../../../adapter-tests/fixtures/methods/array-concat'
|
|
2184
2184
|
import { fixture as arraySliceFixture } from '../../../adapter-tests/fixtures/methods/array-slice'
|
|
2185
|
+
import { fixture as arraySliceCopyFixture } from '../../../adapter-tests/fixtures/methods/array-slice-copy'
|
|
2186
|
+
import { fixture as arrayJoinDefaultFixture } from '../../../adapter-tests/fixtures/methods/array-join-default'
|
|
2187
|
+
import { fixture as arrayAtDefaultFixture } from '../../../adapter-tests/fixtures/methods/array-at-default'
|
|
2188
|
+
import { fixture as arrayConcatCopyFixture } from '../../../adapter-tests/fixtures/methods/array-concat-copy'
|
|
2185
2189
|
import { fixture as arrayReverseFixture } from '../../../adapter-tests/fixtures/methods/array-reverse'
|
|
2186
2190
|
import { fixture as arrayToReversedFixture } from '../../../adapter-tests/fixtures/methods/array-toReversed'
|
|
2187
2191
|
import { fixture as stringToLowerCaseFixture } from '../../../adapter-tests/fixtures/methods/string-toLowerCase'
|
|
2188
2192
|
import { fixture as stringToUpperCaseFixture } from '../../../adapter-tests/fixtures/methods/string-toUpperCase'
|
|
2189
2193
|
import { fixture as stringTrimFixture } from '../../../adapter-tests/fixtures/methods/string-trim'
|
|
2194
|
+
// #1448 Tier B — string methods.
|
|
2195
|
+
import { fixture as stringSplitFixture } from '../../../adapter-tests/fixtures/methods/string-split'
|
|
2196
|
+
import { fixture as stringSplitLimitFixture } from '../../../adapter-tests/fixtures/methods/string-split-limit'
|
|
2197
|
+
import { fixture as stringStartsWithFixture } from '../../../adapter-tests/fixtures/methods/string-startsWith'
|
|
2198
|
+
import { fixture as stringStartsWithPositionFixture } from '../../../adapter-tests/fixtures/methods/string-startsWith-position'
|
|
2199
|
+
import { fixture as stringEndsWithFixture } from '../../../adapter-tests/fixtures/methods/string-endsWith'
|
|
2200
|
+
import { fixture as stringEndsWithPositionFixture } from '../../../adapter-tests/fixtures/methods/string-endsWith-position'
|
|
2201
|
+
import { fixture as stringReplaceFixture } from '../../../adapter-tests/fixtures/methods/string-replace'
|
|
2202
|
+
import { fixture as stringRepeatFixture } from '../../../adapter-tests/fixtures/methods/string-repeat'
|
|
2203
|
+
import { fixture as stringPadStartFixture } from '../../../adapter-tests/fixtures/methods/string-padStart'
|
|
2204
|
+
import { fixture as stringPadEndFixture } from '../../../adapter-tests/fixtures/methods/string-padEnd'
|
|
2190
2205
|
// #1448 Tier B — .sort / .toSorted fixtures.
|
|
2191
2206
|
import { fixture as arraySortFieldAscFixture } from '../../../adapter-tests/fixtures/methods/array-sort-field-asc'
|
|
2192
2207
|
import { fixture as arraySortFieldDescFixture } from '../../../adapter-tests/fixtures/methods/array-sort-field-desc'
|
|
@@ -2214,6 +2229,13 @@ describe('GoTemplateAdapter - #1448 Tier A/B fixture-driven lowering pins', () =
|
|
|
2214
2229
|
{ fixture: arrayAtFixture, expect: 'bf_at .Items (bf_neg 1)' },
|
|
2215
2230
|
{ fixture: arrayConcatFixture, expect: 'bf_concat .Left .Right' },
|
|
2216
2231
|
{ fixture: arraySliceFixture, expect: 'bf_slice .Items 1 3' },
|
|
2232
|
+
// #1448 full-arity — zero-arg defaults. `.slice()` → start 0 (full
|
|
2233
|
+
// copy); `.join()` → default `,` separator.
|
|
2234
|
+
{ fixture: arraySliceCopyFixture, expect: 'bf_slice .Items 0' },
|
|
2235
|
+
{ fixture: arrayJoinDefaultFixture, expect: 'bf_join (.Items) ","' },
|
|
2236
|
+
// `.at()` → index 0; `.concat()` → the receiver (shallow copy).
|
|
2237
|
+
{ fixture: arrayAtDefaultFixture, expect: 'bf_at .Items 0' },
|
|
2238
|
+
{ fixture: arrayConcatCopyFixture, expect: 'bf_join (.Items) "|"' },
|
|
2217
2239
|
{ fixture: arrayReverseFixture, expect: 'bf_reverse .Items' },
|
|
2218
2240
|
// .toReversed shares the helper with .reverse — pinning both
|
|
2219
2241
|
// routings catches a future divergence between them.
|
|
@@ -2221,6 +2243,24 @@ describe('GoTemplateAdapter - #1448 Tier A/B fixture-driven lowering pins', () =
|
|
|
2221
2243
|
{ fixture: stringToLowerCaseFixture,expect: 'bf_lower .Value' },
|
|
2222
2244
|
{ fixture: stringToUpperCaseFixture,expect: 'bf_upper .Value' },
|
|
2223
2245
|
{ fixture: stringTrimFixture, expect: 'bf_trim .Value' },
|
|
2246
|
+
// #1448 Tier B — string → array. `.split(',')` lowers to
|
|
2247
|
+
// `bf_split`, here chained into `.join('|')` so the slice is
|
|
2248
|
+
// observable (`bf_join (bf_split .Value ",") "|"`).
|
|
2249
|
+
{ fixture: stringSplitFixture, expect: 'bf_split .Value ","' },
|
|
2250
|
+
{ fixture: stringSplitLimitFixture, expect: 'bf_split .Value "," 2' },
|
|
2251
|
+
// #1448 Tier B — string → boolean at condition position, so the
|
|
2252
|
+
// emit lands inside `{{if ...}}`.
|
|
2253
|
+
{ fixture: stringStartsWithFixture, expect: '{{if bf_starts_with .Value .Prefix}}' },
|
|
2254
|
+
{ fixture: stringStartsWithPositionFixture, expect: '{{if bf_starts_with .Value "world" 6}}' },
|
|
2255
|
+
{ fixture: stringEndsWithFixture, expect: '{{if bf_ends_with .Value .Suffix}}' },
|
|
2256
|
+
{ fixture: stringEndsWithPositionFixture, expect: '{{if bf_ends_with .Value "hello" 5}}' },
|
|
2257
|
+
// #1448 Tier B — string → string, first-occurrence replace.
|
|
2258
|
+
{ fixture: stringReplaceFixture, expect: 'bf_replace .Value "o" "0"' },
|
|
2259
|
+
// #1448 Tier B — string → string, repeat n times.
|
|
2260
|
+
{ fixture: stringRepeatFixture, expect: 'bf_repeat .Value 3' },
|
|
2261
|
+
// #1448 Tier B — string → string, padded to a target width.
|
|
2262
|
+
{ fixture: stringPadStartFixture, expect: 'bf_pad_start .Value 5 "0"' },
|
|
2263
|
+
{ fixture: stringPadEndFixture, expect: 'bf_pad_end .Value 5 "."' },
|
|
2224
2264
|
// #1448 Tier B — sort / toSorted. Loop-chained shapes wrap the
|
|
2225
2265
|
// iterable in `bf_sort .Items <kind> <key> <type> <dir>`;
|
|
2226
2266
|
// standalone shapes inline the helper at the call site.
|
|
@@ -2278,6 +2318,81 @@ describe('GoTemplateAdapter - #1448 Tier A/B fixture-driven lowering pins', () =
|
|
|
2278
2318
|
// now listed in `UNSUPPORTED_METHODS`, so `isSupported` refuses them
|
|
2279
2319
|
// and `convertExpressionToGo` records BF101 — the same treatment the
|
|
2280
2320
|
// unsupported array methods already got. These tests pin that parity.
|
|
2321
|
+
describe('GoTemplateAdapter - #1448 Tier C reduce field capitalisation', () => {
|
|
2322
|
+
// #1728 review: `bf_reduce`'s projected field name must use the same
|
|
2323
|
+
// initialism-aware capitalisation the adapter applies when generating
|
|
2324
|
+
// struct fields (`capitalizeFieldName`), or the runtime reflect lookup
|
|
2325
|
+
// misses the exported field (`id` → struct `ID`, not `Id`) and folds a
|
|
2326
|
+
// zero value. Pin the emitted key so a regression to plain
|
|
2327
|
+
// first-letter capitalisation fails here.
|
|
2328
|
+
test('reduce over an initialism field emits the Go-initialism key (ID, not Id)', () => {
|
|
2329
|
+
const adapter = new GoTemplateAdapter()
|
|
2330
|
+
const ir = compileToIR(`
|
|
2331
|
+
function C({ items }: { items: { id: number }[] }) {
|
|
2332
|
+
return <div>{items.reduce((sum, x) => sum + x.id, 0)}</div>
|
|
2333
|
+
}
|
|
2334
|
+
export { C }
|
|
2335
|
+
`, adapter)
|
|
2336
|
+
const template = adapter.generate(ir).template ?? ''
|
|
2337
|
+
expect(template).toContain('bf_reduce .Items "+" "field" "ID" "numeric" "0"')
|
|
2338
|
+
expect(template).not.toContain('"Id"')
|
|
2339
|
+
})
|
|
2340
|
+
})
|
|
2341
|
+
|
|
2342
|
+
describe('GoTemplateAdapter - #1448 Tier C .flat(depth?)', () => {
|
|
2343
|
+
function emitFlat(expr: string): string {
|
|
2344
|
+
const adapter = new GoTemplateAdapter()
|
|
2345
|
+
const ir = compileToIR(`
|
|
2346
|
+
function C({ rows }: { rows: number[][] }) {
|
|
2347
|
+
return <div>{${expr}}</div>
|
|
2348
|
+
}
|
|
2349
|
+
export { C }
|
|
2350
|
+
`, adapter)
|
|
2351
|
+
return adapter.generate(ir).template ?? ''
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
test('.flat() emits bf_flat with default depth 1', () => {
|
|
2355
|
+
expect(emitFlat('rows.flat()')).toContain('bf_flat .Rows 1')
|
|
2356
|
+
})
|
|
2357
|
+
|
|
2358
|
+
test('.flat(2) emits the explicit depth', () => {
|
|
2359
|
+
expect(emitFlat('rows.flat(2)')).toContain('bf_flat .Rows 2')
|
|
2360
|
+
})
|
|
2361
|
+
|
|
2362
|
+
test('.flat(Infinity) emits the -1 full-depth sentinel', () => {
|
|
2363
|
+
expect(emitFlat('rows.flat(Infinity)')).toContain('bf_flat .Rows -1')
|
|
2364
|
+
})
|
|
2365
|
+
})
|
|
2366
|
+
|
|
2367
|
+
describe('GoTemplateAdapter - #1448 Tier C .flatMap(field projection)', () => {
|
|
2368
|
+
function emitFlatMap(expr: string): string {
|
|
2369
|
+
const adapter = new GoTemplateAdapter()
|
|
2370
|
+
const ir = compileToIR(`
|
|
2371
|
+
function C({ rows }: { rows: { a: string; b: string; tags: string[] }[] }) {
|
|
2372
|
+
return <div>{${expr}}</div>
|
|
2373
|
+
}
|
|
2374
|
+
export { C }
|
|
2375
|
+
`, adapter)
|
|
2376
|
+
return adapter.generate(ir).template ?? ''
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
test('.flatMap(i => i.field) emits bf_flat_map with the Go-capitalised field', () => {
|
|
2380
|
+
expect(emitFlatMap('rows.flatMap(i => i.tags).join(" ")')).toContain('bf_flat_map .Rows "field" "Tags"')
|
|
2381
|
+
})
|
|
2382
|
+
|
|
2383
|
+
test('.flatMap(i => i) emits the self projection', () => {
|
|
2384
|
+
expect(emitFlatMap('rows.flatMap(i => i).join(" ")')).toContain('bf_flat_map .Rows "self" ""')
|
|
2385
|
+
})
|
|
2386
|
+
|
|
2387
|
+
test('.flatMap(i => [i.a, i.b]) emits bf_flat_map_tuple with capitalised leaves', () => {
|
|
2388
|
+
expect(emitFlatMap('rows.flatMap(i => [i.a, i.b]).join(" ")')).toContain('bf_flat_map_tuple .Rows "field" "A" "field" "B"')
|
|
2389
|
+
})
|
|
2390
|
+
|
|
2391
|
+
test('tuple self + field leaves', () => {
|
|
2392
|
+
expect(emitFlatMap('rows.flatMap(i => [i, i.a]).join(" ")')).toContain('bf_flat_map_tuple .Rows "self" "" "field" "A"')
|
|
2393
|
+
})
|
|
2394
|
+
})
|
|
2395
|
+
|
|
2281
2396
|
describe('GoTemplateAdapter - #1448 @client escape hatch (unsupported methods)', () => {
|
|
2282
2397
|
// Compile a single expression placed in `<div>` text position, with
|
|
2283
2398
|
// and without the directive, and return both the build errors and
|
|
@@ -2323,19 +2438,30 @@ export function C() {
|
|
|
2323
2438
|
// Go fragment that must NOT survive into the template (the pre-fix
|
|
2324
2439
|
// silent-footgun output for the string rows).
|
|
2325
2440
|
const unsupported: Array<{ name: string; expr: string; badEmit: string }> = [
|
|
2326
|
-
// Tier C array methods.
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2441
|
+
// Tier C array methods. The arithmetic-fold `.reduce(fn, init)`
|
|
2442
|
+
// catalogue now lowers (pinned in the positive reduce-* fixtures);
|
|
2443
|
+
// the no-initial-value form stays refused — JS throws on an empty
|
|
2444
|
+
// array there, which a template can't mirror.
|
|
2445
|
+
{ name: 'reduce (no init)', expr: `items().reduce((a, b) => a + b.n)`, badEmit: '.Reduce' },
|
|
2446
|
+
// Self / field / field-tuple `.flatMap` now lowers (#1448 Tier C); a
|
|
2447
|
+
// tuple with a non-leaf element (here a string literal) stays refused.
|
|
2448
|
+
{ name: 'flatMap (literal element)', expr: `items().flatMap(i => [i.name, "x"])`, badEmit: '.FlatMap' },
|
|
2449
|
+
// Lowered methods whose MEANINGFUL extra argument isn't lowered yet
|
|
2450
|
+
// (#1448): the `fromIndex` of `.includes`/`.indexOf`/`.lastIndexOf`
|
|
2451
|
+
// and the variadic `.concat`. The parser refuses these (silently
|
|
2452
|
+
// dropping the arg would change the result) rather than emitting a
|
|
2453
|
+
// render-crashing `.Includes` / `.Concat` field access. (The
|
|
2454
|
+
// zero-arg defaults `.join()`/`.slice()` and JS-ignored trailing
|
|
2455
|
+
// args like `.trim(1)` are accepted — pinned in the positive blocks.)
|
|
2456
|
+
{ name: 'includes (2-arg fromIndex)', expr: `items().includes("a", 1)`, badEmit: '.Includes' },
|
|
2457
|
+
{ name: 'concat (variadic)', expr: `items().concat(items(), items())`, badEmit: '.Concat' },
|
|
2330
2458
|
// Tier B/C string methods — previously slipped through with no
|
|
2331
|
-
// diagnostic; now gated by `UNSUPPORTED_METHODS`.
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
{ name: 'padStart', expr: `name().padStart(5, "0")`, badEmit: '.Name.PadStart' },
|
|
2338
|
-
{ name: 'padEnd', expr: `name().padEnd(5, "0")`, badEmit: '.Name.PadEnd' },
|
|
2459
|
+
// diagnostic; now gated by `UNSUPPORTED_METHODS`. The full Tier B
|
|
2460
|
+
// string set (`split`, `startsWith`, `endsWith`, `replace`,
|
|
2461
|
+
// `repeat`, `padStart`, `padEnd`) has since landed its full-arity
|
|
2462
|
+
// lowering and moved to the positive fixture-pin block above. The
|
|
2463
|
+
// regex-pattern `replace` form is pinned separately below; `charAt`
|
|
2464
|
+
// is Tier C and stays refused entirely.
|
|
2339
2465
|
{ name: 'charAt', expr: `name().charAt(0)`, badEmit: '.Name.CharAt' },
|
|
2340
2466
|
]
|
|
2341
2467
|
for (const { name, expr, badEmit } of unsupported) {
|
|
@@ -2354,23 +2480,76 @@ export function C() {
|
|
|
2354
2480
|
})
|
|
2355
2481
|
}
|
|
2356
2482
|
|
|
2483
|
+
// The diagnostic's `suggestion.message` is shaped by `isSupported`'s
|
|
2484
|
+
// `selfContained` flag: reasons that already spell out the fix are shown
|
|
2485
|
+
// as-is, while low-level parser reasons still get the adapter's generic
|
|
2486
|
+
// "Options" remediation appended so users never lose actionable steps.
|
|
2487
|
+
function suggestionFor(expr: string): string {
|
|
2488
|
+
const { errors } = emit(expr, false)
|
|
2489
|
+
const e = errors.find(e => e.code === 'BF101' || e.code === 'BF102')
|
|
2490
|
+
return e?.suggestion?.message ?? ''
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
test('self-contained reason is shown without the generic Options block', () => {
|
|
2494
|
+
// Generic unsupported-method reason already carries the remedy.
|
|
2495
|
+
const msg = suggestionFor('items().reduce((a, b) => a + b.n)')
|
|
2496
|
+
expect(msg).toContain('no SSR')
|
|
2497
|
+
expect(msg).not.toContain('Options:')
|
|
2498
|
+
})
|
|
2499
|
+
|
|
2500
|
+
test('tailored forEach reason keeps its own guidance, no Options block', () => {
|
|
2501
|
+
const msg = suggestionFor('items().forEach(x => x)')
|
|
2502
|
+
expect(msg).toContain("'.map(")
|
|
2503
|
+
// The forEach message deliberately steers away from @client; the
|
|
2504
|
+
// generic Options block (which re-suggests it) must not be appended.
|
|
2505
|
+
expect(msg).not.toContain('Options:')
|
|
2506
|
+
expect(msg).not.toContain('@client')
|
|
2507
|
+
})
|
|
2508
|
+
|
|
2509
|
+
test('low-level reason still gets the actionable Options block appended', () => {
|
|
2510
|
+
// `typeof` has no structured remedy reason → users must keep the
|
|
2511
|
+
// generic next steps (regression guard for #1730 review).
|
|
2512
|
+
const msg = suggestionFor('typeof items()')
|
|
2513
|
+
expect(msg).toContain('Options:')
|
|
2514
|
+
expect(msg).toContain('@client')
|
|
2515
|
+
})
|
|
2516
|
+
|
|
2357
2517
|
// Predicate-level use of an unsupported string method also fails the
|
|
2358
|
-
// build loudly (intended): a `.filter(t => t.name.
|
|
2518
|
+
// build loudly (intended): a `.filter(t => t.name.charAt(0) === "a")`
|
|
2359
2519
|
// whose predicate calls one of the gated methods now refuses the whole
|
|
2360
2520
|
// loop with BF101 (via the shared `isSupported` predicate gate in
|
|
2361
|
-
// jsx-to-ir) rather than lowering to a broken `.
|
|
2521
|
+
// jsx-to-ir) rather than lowering to a broken `.CharAt` inside the
|
|
2362
2522
|
// range. Pinning this so the loud-failure contract can't silently
|
|
2363
|
-
// regress back to the old emit-broken-template behaviour.
|
|
2523
|
+
// regress back to the old emit-broken-template behaviour. (`charAt`
|
|
2524
|
+
// is a Tier C method that stays refused — earlier this test used
|
|
2525
|
+
// `startsWith`, which has since landed its Tier B lowering.)
|
|
2364
2526
|
test('unsupported string method inside a .filter() predicate raises BF101', () => {
|
|
2365
2527
|
const result = compileJSX(`
|
|
2366
2528
|
"use client"
|
|
2367
2529
|
import { createSignal } from "@barefootjs/client"
|
|
2368
2530
|
export function C() {
|
|
2369
2531
|
const [items, setItems] = createSignal<{ name: string }[]>([])
|
|
2370
|
-
return <ul>{items().filter(t => t.name.
|
|
2532
|
+
return <ul>{items().filter(t => t.name.charAt(0) === "a").map(t => <li key={t.name}>{t.name}</li>)}</ul>
|
|
2533
|
+
}
|
|
2534
|
+
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter() })
|
|
2535
|
+
expect(result.errors?.some(e => e.code === 'BF101')).toBe(true)
|
|
2536
|
+
})
|
|
2537
|
+
|
|
2538
|
+
// The string-pattern form of `.replace` lowers (#1448 Tier B), but
|
|
2539
|
+
// the regex-pattern form stays refused with BF101 — the Perl `s///`
|
|
2540
|
+
// vs Go `regexp.ReplaceAllString` flavour gap is the open design
|
|
2541
|
+
// question. Pinning the refusal so the string-form lowering can't
|
|
2542
|
+
// accidentally start emitting a broken `.Replace` for the regex form.
|
|
2543
|
+
test('regex-pattern .replace raises BF101 (string-pattern form is lowered)', () => {
|
|
2544
|
+
const result = compileJSX(`
|
|
2545
|
+
function C({ value }: { value: string }) {
|
|
2546
|
+
return <div>{value.replace(/o/g, "0")}</div>
|
|
2371
2547
|
}
|
|
2548
|
+
export { C }
|
|
2372
2549
|
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter() })
|
|
2373
2550
|
expect(result.errors?.some(e => e.code === 'BF101')).toBe(true)
|
|
2551
|
+
const template = result.files?.find(f => f.path.endsWith('.tmpl'))?.content ?? ''
|
|
2552
|
+
expect(template).not.toContain('.Replace')
|
|
2374
2553
|
})
|
|
2375
2554
|
|
|
2376
2555
|
// Tier B `.sort` / `.toSorted` follow-ups still refused with BF021.
|
|
@@ -2404,7 +2583,9 @@ export function C() {
|
|
|
2404
2583
|
// more `can't evaluate field …` crash), so we assert the build error
|
|
2405
2584
|
// rather than a render crash. Skipped on hosts without Go.
|
|
2406
2585
|
test('e2e: @client renders placeholder; bare is caught at build with BF101', async () => {
|
|
2407
|
-
|
|
2586
|
+
// Uses the Tier C `charAt` (still refused) — earlier this test used
|
|
2587
|
+
// `repeat`, which has since landed its #1448 Tier B lowering.
|
|
2588
|
+
const bare = emit(`name().charAt(0)`, false)
|
|
2408
2589
|
expect(bare.errors.some(e => e.code === 'BF101')).toBe(true)
|
|
2409
2590
|
|
|
2410
2591
|
try {
|
|
@@ -2414,7 +2595,7 @@ export function C() {
|
|
|
2414
2595
|
import { createSignal } from "@barefootjs/client"
|
|
2415
2596
|
export function C() {
|
|
2416
2597
|
const [name, setName] = createSignal("hello")
|
|
2417
|
-
return <div>{/* @client */ name().
|
|
2598
|
+
return <div>{/* @client */ name().charAt(0)}</div>
|
|
2418
2599
|
}
|
|
2419
2600
|
`.trimStart(),
|
|
2420
2601
|
adapter: new GoTemplateAdapter(),
|
|
@@ -27,6 +27,9 @@ import type {
|
|
|
27
27
|
ParsedExpr,
|
|
28
28
|
ParsedStatement,
|
|
29
29
|
SortComparator,
|
|
30
|
+
ReduceOp,
|
|
31
|
+
FlatDepth,
|
|
32
|
+
FlatMapOp,
|
|
30
33
|
TemplatePart,
|
|
31
34
|
IRIfStatement,
|
|
32
35
|
IRProvider,
|
|
@@ -45,6 +48,7 @@ import {
|
|
|
45
48
|
type IRNodeEmitter,
|
|
46
49
|
type EmitIRNode,
|
|
47
50
|
type AttrValueEmitter,
|
|
51
|
+
type SupportResult,
|
|
48
52
|
isBooleanAttr,
|
|
49
53
|
parseExpression,
|
|
50
54
|
isSupported,
|
|
@@ -208,8 +212,56 @@ function emitBfSort(recv: string, c: SortComparator): string {
|
|
|
208
212
|
return `bf_sort ${wrapIfMultiToken(recv)} ${groups.join(' ')}`
|
|
209
213
|
}
|
|
210
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Emit the `bf_reduce` call for a `.reduce(fn, init)` arithmetic fold
|
|
217
|
+
* (#1448 Tier C):
|
|
218
|
+
*
|
|
219
|
+
* bf_reduce <recv> "<op>" "<keyKind>" "<keyName>" "<type>" "<init>" "<direction>"
|
|
220
|
+
*
|
|
221
|
+
* op: "+" | "*"
|
|
222
|
+
* keyKind: "self" | "field"
|
|
223
|
+
* keyName: "" when keyKind=self; capitalised field name otherwise
|
|
224
|
+
* (matches the Go struct-field convention, mirroring
|
|
225
|
+
* `emitBfSort`)
|
|
226
|
+
* type: "numeric" | "string"
|
|
227
|
+
* init: the fold's start value — the numeric literal's text for a
|
|
228
|
+
* numeric fold, or the string literal's contents for a
|
|
229
|
+
* concat fold
|
|
230
|
+
* direction: "left" (reduce) | "right" (reduceRight)
|
|
231
|
+
*
|
|
232
|
+
* The runtime folds `init <op> key(item)` in `direction` order and
|
|
233
|
+
* returns the accumulated value (float64 for numeric, string for
|
|
234
|
+
* concat). The order is only observable for string concat — numeric
|
|
235
|
+
* sum / product commute.
|
|
236
|
+
*/
|
|
237
|
+
function emitBfReduce(recv: string, op: ReduceOp, direction: 'left' | 'right'): string {
|
|
238
|
+
const keyName = op.key.kind === 'field' ? capitalize(op.key.field) : ''
|
|
239
|
+
// `op.init` is already the decoded seed value (canonical decimal for
|
|
240
|
+
// numeric folds — `strconv.ParseFloat`-safe; escape-free contents for
|
|
241
|
+
// concat folds). Pass it as a quoted operand the runtime interprets
|
|
242
|
+
// by `type`. `direction` is "left" (reduce) or "right" (reduceRight)
|
|
243
|
+
// — only observable for string concatenation; numeric folds are
|
|
244
|
+
// commutative.
|
|
245
|
+
return `bf_reduce ${wrapIfMultiToken(recv)} "${op.op}" "${op.key.kind}" "${keyName}" "${op.type}" "${escapeGoString(op.init)}" "${direction}"`
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Escape a value for embedding in a Go-template double-quoted string. */
|
|
249
|
+
function escapeGoString(s: string): string {
|
|
250
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
|
251
|
+
}
|
|
252
|
+
|
|
211
253
|
function capitalize(s: string): string {
|
|
212
|
-
|
|
254
|
+
if (s.length === 0) return s
|
|
255
|
+
// Match the adapter's struct-field naming (`capitalizeFieldName`):
|
|
256
|
+
// a whole-word Go initialism uppercases entirely (`id` → `ID`,
|
|
257
|
+
// `url` → `URL`) so the `bf_sort` / `bf_reduce` reflect lookup
|
|
258
|
+
// resolves the generated exported field instead of silently folding
|
|
259
|
+
// a zero value. The class is fully initialised by the time any emit
|
|
260
|
+
// helper runs, so referencing the static set here is safe.
|
|
261
|
+
if (GoTemplateAdapter.GO_INITIALISMS.has(s.toLowerCase())) {
|
|
262
|
+
return s.toUpperCase()
|
|
263
|
+
}
|
|
264
|
+
return s[0].toUpperCase() + s.slice(1)
|
|
213
265
|
}
|
|
214
266
|
|
|
215
267
|
/**
|
|
@@ -239,6 +291,22 @@ interface PrimitiveSpec {
|
|
|
239
291
|
emit: (args: string[]) => string
|
|
240
292
|
}
|
|
241
293
|
|
|
294
|
+
// Generic remediation appended to BF101 / BF102 diagnostics whose reason
|
|
295
|
+
// doesn't already carry actionable next steps.
|
|
296
|
+
const GO_REMEDIATION_OPTIONS =
|
|
297
|
+
'Options:\n1. Use @client directive for client-side evaluation\n2. Pre-compute the value in Go code'
|
|
298
|
+
|
|
299
|
+
// Build the `suggestion.message` for an unsupported expression/condition.
|
|
300
|
+
// A self-contained reason (it already spells out the fix — e.g. the
|
|
301
|
+
// pre-compute / @client hint or the tailored forEach message) is shown
|
|
302
|
+
// as-is; a low-level reason gets the generic options appended; with no
|
|
303
|
+
// reason at all we fall back to the options alone.
|
|
304
|
+
function buildUnsupportedSuggestion(support: SupportResult): string {
|
|
305
|
+
if (!support.reason) return GO_REMEDIATION_OPTIONS
|
|
306
|
+
if (support.selfContained) return support.reason
|
|
307
|
+
return `${support.reason}\n\n${GO_REMEDIATION_OPTIONS}`
|
|
308
|
+
}
|
|
309
|
+
|
|
242
310
|
const GO_TEMPLATE_PRIMITIVES: Record<string, PrimitiveSpec> = {
|
|
243
311
|
'JSON.stringify': { arity: 1, emit: (args) => `bf_json ${args[0]}` },
|
|
244
312
|
'String': { arity: 1, emit: (args) => `bf_string ${args[0]}` },
|
|
@@ -2579,7 +2647,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2579
2647
|
private static GO_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
2580
2648
|
|
|
2581
2649
|
/** Go common initialisms that should be fully uppercased (https://go.dev/wiki/CodeReviewComments#initialisms) */
|
|
2582
|
-
private
|
|
2650
|
+
// Not `private`: the module-level `capitalize` helper reads this so
|
|
2651
|
+
// `bf_sort` / `bf_reduce` field projection matches `capitalizeFieldName`.
|
|
2652
|
+
static GO_INITIALISMS = new Set([
|
|
2583
2653
|
'id', 'url', 'http', 'https', 'api', 'json', 'xml', 'html', 'css', 'sql',
|
|
2584
2654
|
'ip', 'tcp', 'udp', 'dns', 'ssh', 'tls', 'ssl', 'uri', 'uid', 'uuid',
|
|
2585
2655
|
'ascii', 'utf8', 'eof', 'grpc', 'rpc', 'cpu', 'gpu', 'ram', 'os',
|
|
@@ -3089,7 +3159,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3089
3159
|
message: `Higher-order method '.${method}' shape cannot be lowered to a Go template action`,
|
|
3090
3160
|
loc: this.makeLoc(),
|
|
3091
3161
|
suggestion: {
|
|
3092
|
-
message:
|
|
3162
|
+
message: GO_REMEDIATION_OPTIONS,
|
|
3093
3163
|
},
|
|
3094
3164
|
})
|
|
3095
3165
|
return `""`
|
|
@@ -3109,7 +3179,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3109
3179
|
switch (method) {
|
|
3110
3180
|
case 'join': {
|
|
3111
3181
|
const obj = emit(object)
|
|
3112
|
-
|
|
3182
|
+
// `.join()` defaults the separator to `,` (JS); any extra
|
|
3183
|
+
// argument is ignored. Only `args[0]` is read.
|
|
3184
|
+
const sep = args.length >= 1 ? emit(args[0]) : '","'
|
|
3113
3185
|
// Both operands need paren-wrapping when they emit a
|
|
3114
3186
|
// multi-token prefix-call form (e.g. `sep` lowering to
|
|
3115
3187
|
// `bf_trim .Raw` would make Go template parse
|
|
@@ -3145,30 +3217,36 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3145
3217
|
// `.at(i)` supports negative indices (`.at(-1)` → last
|
|
3146
3218
|
// element). The Go `bf_at` helper was already registered in
|
|
3147
3219
|
// the FuncMap for the runtime — this PR wires it to the JS
|
|
3148
|
-
// method name at the adapter layer.
|
|
3220
|
+
// method name at the adapter layer. `.at()` with no argument is
|
|
3221
|
+
// `.at(0)` (the first element); extra arguments are ignored.
|
|
3149
3222
|
const obj = emit(object)
|
|
3150
|
-
const idx = emit(args[0])
|
|
3223
|
+
const idx = args.length >= 1 ? emit(args[0]) : '0'
|
|
3151
3224
|
return `bf_at ${wrapIfMultiToken(obj)} ${wrapIfMultiToken(idx)}`
|
|
3152
3225
|
}
|
|
3153
3226
|
case 'concat': {
|
|
3154
3227
|
// `.concat(other)` merges two arrays. The runtime helper
|
|
3155
3228
|
// `bf_concat` reflects over both operands so callers can
|
|
3156
3229
|
// mix `[]string` + `[]string` or `[]any` + `[]string` etc.
|
|
3157
|
-
// without per-call-site type-juggling.
|
|
3230
|
+
// without per-call-site type-juggling. `.concat()` with no
|
|
3231
|
+
// argument is a shallow copy — indistinguishable from the
|
|
3232
|
+
// receiver in an SSR snapshot, so it lowers to the receiver.
|
|
3233
|
+
if (args.length === 0) {
|
|
3234
|
+
return emit(object)
|
|
3235
|
+
}
|
|
3158
3236
|
const a = emit(object)
|
|
3159
3237
|
const b = emit(args[0])
|
|
3160
3238
|
return `bf_concat ${wrapIfMultiToken(a)} ${wrapIfMultiToken(b)}`
|
|
3161
3239
|
}
|
|
3162
3240
|
case 'slice': {
|
|
3163
|
-
// `.slice(start)` / `.slice(start, end)` —
|
|
3164
|
-
// through `bf_slice`.
|
|
3165
|
-
//
|
|
3166
|
-
//
|
|
3167
|
-
//
|
|
3168
|
-
//
|
|
3241
|
+
// `.slice()` / `.slice(start)` / `.slice(start, end)` — route
|
|
3242
|
+
// through `bf_slice`. A missing `start` defaults to 0 (full
|
|
3243
|
+
// copy); the runtime helper treats an absent `end` as "to
|
|
3244
|
+
// length". Out-of-bounds indices clamp instead of panicking
|
|
3245
|
+
// (JS-compat); `start > end` returns an empty slice. JS ignores
|
|
3246
|
+
// a third+ argument, so only `args[0]` / `args[1]` are read.
|
|
3169
3247
|
const recv = emit(object)
|
|
3170
|
-
const start = emit(args[0])
|
|
3171
|
-
if (args.length
|
|
3248
|
+
const start = args.length >= 1 ? emit(args[0]) : '0'
|
|
3249
|
+
if (args.length <= 1) {
|
|
3172
3250
|
return `bf_slice ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(start)}`
|
|
3173
3251
|
}
|
|
3174
3252
|
const end = emit(args[1])
|
|
@@ -3204,6 +3282,78 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3204
3282
|
const recv = emit(object)
|
|
3205
3283
|
return `bf_trim ${wrapIfMultiToken(recv)}`
|
|
3206
3284
|
}
|
|
3285
|
+
case 'split': {
|
|
3286
|
+
// `.split()` / `.split(sep)` / `.split(sep, limit)` — string →
|
|
3287
|
+
// `[]any`. No separator → the whole string as a single element
|
|
3288
|
+
// (`bf_arr`). Otherwise `bf_split` (wraps `strings.Split`,
|
|
3289
|
+
// normalised to `[]any`); a second `limit` argument caps the
|
|
3290
|
+
// pieces. JS ignores a third+ argument. See #1448 Tier B.
|
|
3291
|
+
const recv = emit(object)
|
|
3292
|
+
if (args.length === 0) {
|
|
3293
|
+
return `bf_arr ${wrapIfMultiToken(recv)}`
|
|
3294
|
+
}
|
|
3295
|
+
const sep = emit(args[0])
|
|
3296
|
+
if (args.length === 1) {
|
|
3297
|
+
return `bf_split ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(sep)}`
|
|
3298
|
+
}
|
|
3299
|
+
const limit = emit(args[1])
|
|
3300
|
+
return `bf_split ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(sep)} ${wrapIfMultiToken(limit)}`
|
|
3301
|
+
}
|
|
3302
|
+
case 'startsWith':
|
|
3303
|
+
case 'endsWith': {
|
|
3304
|
+
// `.startsWith(prefix, position?)` / `.endsWith(suffix,
|
|
3305
|
+
// endPosition?)` — string → boolean via the `bf_starts_with` /
|
|
3306
|
+
// `bf_ends_with` helpers (`strings.HasPrefix` /
|
|
3307
|
+
// `strings.HasSuffix`). The optional second argument re-anchors
|
|
3308
|
+
// the test; JS ignores a third+ argument. See #1448 Tier B.
|
|
3309
|
+
const fn = method === 'startsWith' ? 'bf_starts_with' : 'bf_ends_with'
|
|
3310
|
+
const recv = emit(object)
|
|
3311
|
+
const arg = emit(args[0])
|
|
3312
|
+
const base = `${fn} ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(arg)}`
|
|
3313
|
+
if (args.length >= 2) {
|
|
3314
|
+
return `${base} ${wrapIfMultiToken(emit(args[1]))}`
|
|
3315
|
+
}
|
|
3316
|
+
return base
|
|
3317
|
+
}
|
|
3318
|
+
case 'replace': {
|
|
3319
|
+
// `.replace(old, new)` — string-pattern form, first occurrence
|
|
3320
|
+
// only, via the new `bf_replace` helper (`strings.Replace` with
|
|
3321
|
+
// n=1). The regex-pattern form is refused upstream at the
|
|
3322
|
+
// parser. See #1448 Tier B.
|
|
3323
|
+
const recv = emit(object)
|
|
3324
|
+
const oldS = emit(args[0])
|
|
3325
|
+
const newS = emit(args[1])
|
|
3326
|
+
return `bf_replace ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(oldS)} ${wrapIfMultiToken(newS)}`
|
|
3327
|
+
}
|
|
3328
|
+
case 'repeat': {
|
|
3329
|
+
// `.repeat(n)` — string repeated `n` times via the `bf_repeat`
|
|
3330
|
+
// helper. The helper clamps a negative count to "" instead of
|
|
3331
|
+
// letting `strings.Repeat` panic. Full JS arity: the no-argument
|
|
3332
|
+
// form is `repeat(0)` → ""; a second+ argument is ignored.
|
|
3333
|
+
// See #1448 Tier B.
|
|
3334
|
+
const recv = emit(object)
|
|
3335
|
+
const count = args.length === 0 ? '0' : emit(args[0])
|
|
3336
|
+
return `bf_repeat ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(count)}`
|
|
3337
|
+
}
|
|
3338
|
+
case 'padStart':
|
|
3339
|
+
case 'padEnd': {
|
|
3340
|
+
// `.padStart(target, pad?)` / `.padEnd(target, pad?)` — pad to
|
|
3341
|
+
// `target` runes with `pad` (default a single space, supplied
|
|
3342
|
+
// by the variadic helper when the arg is absent). Full JS arity:
|
|
3343
|
+
// the no-argument form is `padStart(0)` → the receiver
|
|
3344
|
+
// unchanged; a third+ argument is ignored. See #1448 Tier B.
|
|
3345
|
+
const fn = method === 'padStart' ? 'bf_pad_start' : 'bf_pad_end'
|
|
3346
|
+
const recv = emit(object)
|
|
3347
|
+
if (args.length === 0) {
|
|
3348
|
+
return `${fn} ${wrapIfMultiToken(recv)} 0`
|
|
3349
|
+
}
|
|
3350
|
+
const target = emit(args[0])
|
|
3351
|
+
if (args.length === 1) {
|
|
3352
|
+
return `${fn} ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(target)}`
|
|
3353
|
+
}
|
|
3354
|
+
const pad = emit(args[1])
|
|
3355
|
+
return `${fn} ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(target)} ${wrapIfMultiToken(pad)}`
|
|
3356
|
+
}
|
|
3207
3357
|
default: {
|
|
3208
3358
|
const _exhaustive: never = method
|
|
3209
3359
|
throw new Error(`Go arrayMethod: unhandled ArrayMethod '${(_exhaustive as string)}'`)
|
|
@@ -3231,6 +3381,51 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3231
3381
|
return emitBfSort(emit(object), comparator)
|
|
3232
3382
|
}
|
|
3233
3383
|
|
|
3384
|
+
reduceMethod(
|
|
3385
|
+
method: 'reduce' | 'reduceRight',
|
|
3386
|
+
object: ParsedExpr,
|
|
3387
|
+
reduceOp: ReduceOp,
|
|
3388
|
+
emit: (e: ParsedExpr) => string,
|
|
3389
|
+
): string {
|
|
3390
|
+
// `.reduce(fn, init)` / `.reduceRight(fn, init)` arithmetic-fold
|
|
3391
|
+
// lowering (#1448 Tier C). The structured `ReduceOp` (op / key /
|
|
3392
|
+
// type / init) plus the fold direction feed the `bf_reduce` runtime
|
|
3393
|
+
// helper, which folds the receiver into a scalar.
|
|
3394
|
+
const direction = method === 'reduceRight' ? 'right' : 'left'
|
|
3395
|
+
return emitBfReduce(emit(object), reduceOp, direction)
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
|
|
3399
|
+
// `.flat(depth?)` → `bf_flat <recv> <depth>`. The `Infinity` form
|
|
3400
|
+
// lowers to the `-1` sentinel (flatten fully); a finite depth flattens
|
|
3401
|
+
// that many levels (`0` = shallow copy). See packages/adapter-go-
|
|
3402
|
+
// template/runtime/bf.go.
|
|
3403
|
+
const d = depth === 'infinity' ? -1 : depth
|
|
3404
|
+
return `bf_flat ${wrapIfMultiToken(emit(object))} ${d}`
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
flatMapMethod(object: ParsedExpr, op: FlatMapOp, emit: (e: ParsedExpr) => string): string {
|
|
3408
|
+
const recv = wrapIfMultiToken(emit(object))
|
|
3409
|
+
const proj = op.projection
|
|
3410
|
+
// Tuple projection `i => [i.a, i.b]` → `bf_flat_map_tuple <recv>
|
|
3411
|
+
// "<kind>" "<name>" ...` (one quoted pair per leaf). flat(1) removes
|
|
3412
|
+
// only the literal's wrapper, so the runtime appends each leaf verbatim.
|
|
3413
|
+
if (proj.kind === 'tuple') {
|
|
3414
|
+
const pairs = proj.elements
|
|
3415
|
+
.map(l => (l.kind === 'self' ? `"self" ""` : `"field" "${capitalize(l.field)}"`))
|
|
3416
|
+
.join(' ')
|
|
3417
|
+
return `bf_flat_map_tuple ${recv} ${pairs}`
|
|
3418
|
+
}
|
|
3419
|
+
// Scalar `.flatMap(i => i)` / `.flatMap(i => i.field)` → `bf_flat_map
|
|
3420
|
+
// <recv> "<kind>" "<name>"`. The runtime projects each item then
|
|
3421
|
+
// flattens one level. The field name uses the Go struct-field
|
|
3422
|
+
// capitalisation, matching `bf_reduce` / `bf_sort`.
|
|
3423
|
+
if (proj.kind === 'self') {
|
|
3424
|
+
return `bf_flat_map ${recv} "self" ""`
|
|
3425
|
+
}
|
|
3426
|
+
return `bf_flat_map ${recv} "field" "${capitalize(proj.field)}"`
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3234
3429
|
unsupported(raw: string, _reason: string): string {
|
|
3235
3430
|
// Should not happen if `isSupported` was checked at parse time.
|
|
3236
3431
|
return `[UNSUPPORTED: ${raw}]`
|
|
@@ -3966,9 +4161,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3966
4161
|
message: `Expression not supported: ${trimmed}`,
|
|
3967
4162
|
loc: this.makeLoc(),
|
|
3968
4163
|
suggestion: {
|
|
3969
|
-
message: support
|
|
3970
|
-
? `${support.reason}\n\nOptions:\n1. Use @client directive for client-side evaluation\n2. Pre-compute the value in Go code`
|
|
3971
|
-
: 'Options:\n1. Use @client directive for client-side evaluation\n2. Pre-compute the value in Go code',
|
|
4164
|
+
message: buildUnsupportedSuggestion(support),
|
|
3972
4165
|
},
|
|
3973
4166
|
})
|
|
3974
4167
|
// Return empty string - Go template comments must be separate actions
|
|
@@ -4006,7 +4199,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4006
4199
|
message: `Complex predicate in else-if is not supported: ${altIfStmt.condition}`,
|
|
4007
4200
|
loc: this.makeLoc(),
|
|
4008
4201
|
suggestion: {
|
|
4009
|
-
message:
|
|
4202
|
+
message: GO_REMEDIATION_OPTIONS,
|
|
4010
4203
|
},
|
|
4011
4204
|
})
|
|
4012
4205
|
}
|
|
@@ -4103,9 +4296,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4103
4296
|
message: `Condition not supported: ${trimmed}`,
|
|
4104
4297
|
loc: this.makeLoc(),
|
|
4105
4298
|
suggestion: {
|
|
4106
|
-
message: support
|
|
4107
|
-
? `${support.reason}\n\nOptions:\n1. Use @client directive for client-side evaluation\n2. Pre-compute the value in Go code`
|
|
4108
|
-
: 'Expression contains unsupported syntax',
|
|
4299
|
+
message: buildUnsupportedSuggestion(support),
|
|
4109
4300
|
},
|
|
4110
4301
|
})
|
|
4111
4302
|
// Return false - Go template comments must be separate actions
|