@barefootjs/go-template 0.18.5 → 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/adapter/analysis/static-child-loop-bake.d.ts +61 -0
- package/dist/adapter/analysis/static-child-loop-bake.d.ts.map +1 -0
- package/dist/adapter/analysis/static-element-loop-bake.d.ts +83 -0
- package/dist/adapter/analysis/static-element-loop-bake.d.ts.map +1 -0
- package/dist/adapter/go-template-adapter.d.ts +116 -2
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +339 -32
- package/dist/adapter/lib/compile-state.d.ts +8 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/props/prop-classes.d.ts +28 -9
- package/dist/adapter/props/prop-classes.d.ts.map +1 -1
- package/dist/build.js +339 -32
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +341 -41
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/go-template-adapter.test.ts +707 -3
- package/src/adapter/analysis/static-child-loop-bake.ts +119 -0
- package/src/adapter/analysis/static-element-loop-bake.ts +211 -0
- package/src/adapter/go-template-adapter.ts +435 -25
- package/src/adapter/lib/compile-state.ts +9 -0
- package/src/adapter/props/prop-classes.ts +34 -9
- package/src/conformance-pins.ts +30 -31
- package/src/render-divergences.ts +12 -0
- package/src/test-render.ts +109 -6
|
@@ -3239,9 +3239,10 @@ export { C }
|
|
|
3239
3239
|
expect(t).toContain('bf_map_eval .Users')
|
|
3240
3240
|
})
|
|
3241
3241
|
|
|
3242
|
-
// The function-reference `.map(format)`
|
|
3243
|
-
//
|
|
3244
|
-
//
|
|
3242
|
+
// The function-reference `.map(format)` case is now covered cross-adapter
|
|
3243
|
+
// by the `array-map-function-reference` shared fixture — `format` resolves
|
|
3244
|
+
// to its declaration (#2206) and the fixture compiles clean rather than
|
|
3245
|
+
// refusing with BF101.
|
|
3245
3246
|
})
|
|
3246
3247
|
|
|
3247
3248
|
describe('GoTemplateAdapter - #1448 Tier C .flatMap(field projection)', () => {
|
|
@@ -3865,3 +3866,706 @@ export function TodoList(props: { todos?: Todo[] }) {
|
|
|
3865
3866
|
expect(types).toContain('TodoItems []')
|
|
3866
3867
|
})
|
|
3867
3868
|
})
|
|
3869
|
+
|
|
3870
|
+
// #2228: a `.filter(t => …).map(todo => <Child todo={todo} .../>)` loop whose
|
|
3871
|
+
// body is a single child component ranges the WRAPPER slice (`.TodoItems`,
|
|
3872
|
+
// `.{ChildName}s` — see #2130 above), so `{{if}}`'s dot context for the
|
|
3873
|
+
// filter predicate is the wrapper `TodoItemProps` struct, not the raw datum.
|
|
3874
|
+
// `t.done` used to lower straight to `.Done` regardless — a field that only
|
|
3875
|
+
// exists on the raw `Todo`, nested under whichever prop forwards the loop
|
|
3876
|
+
// param verbatim (`todo={todo}` → `.Todo`). `html/template` resolves struct
|
|
3877
|
+
// fields at EXECUTE time, not Go-compile time, so this shipped silently
|
|
3878
|
+
// until a predicate branch that isn't short-circuited away actually ran
|
|
3879
|
+
// (discovered via #2209's `buildDynamicChildLoopSeeding`, which populates
|
|
3880
|
+
// `.TodoItems` in the test harness — previously always empty).
|
|
3881
|
+
describe('GoTemplateAdapter - filter predicate qualifies through wrapper-slice datum field (#2228)', () => {
|
|
3882
|
+
// Same block-body filter shape as TodoAppSSR.tsx (`filter(t => { const f =
|
|
3883
|
+
// filter(); if (f === 'active') return !t.done; if (f === 'completed')
|
|
3884
|
+
// return t.done; return true })`), folded to one expression by #2040's
|
|
3885
|
+
// `predicateTernaryToLogical`. `filter`'s SSR default is `'active'` (not
|
|
3886
|
+
// `'all'`) specifically so `!t.done` is REACHABLE — `'all'`'s short-circuit
|
|
3887
|
+
// is exactly what hid this bug in the shipped todo-app-ssr fixture.
|
|
3888
|
+
const TODO_FILTER_PROBE_SOURCE = `
|
|
3889
|
+
'use client'
|
|
3890
|
+
import { createSignal } from '@barefootjs/client'
|
|
3891
|
+
import { TodoItem } from './todo-item'
|
|
3892
|
+
|
|
3893
|
+
type Todo = { id: number; text: string; done: boolean }
|
|
3894
|
+
type Filter = 'all' | 'active' | 'completed'
|
|
3895
|
+
|
|
3896
|
+
export function TodoFilterProbe(props: { initialTodos?: Todo[] }) {
|
|
3897
|
+
const [todos] = createSignal<Todo[]>(props.initialTodos ?? [])
|
|
3898
|
+
const [filter] = createSignal<Filter>('active')
|
|
3899
|
+
|
|
3900
|
+
return (
|
|
3901
|
+
<ul>
|
|
3902
|
+
{todos().filter(t => {
|
|
3903
|
+
const f = filter()
|
|
3904
|
+
if (f === 'active') return !t.done
|
|
3905
|
+
if (f === 'completed') return t.done
|
|
3906
|
+
return true
|
|
3907
|
+
}).map(todo => (
|
|
3908
|
+
<TodoItem key={todo.id} todo={todo} />
|
|
3909
|
+
))}
|
|
3910
|
+
</ul>
|
|
3911
|
+
)
|
|
3912
|
+
}
|
|
3913
|
+
`
|
|
3914
|
+
|
|
3915
|
+
const TODO_ITEM_SOURCE = `
|
|
3916
|
+
type Todo = { id: number; text: string; done: boolean }
|
|
3917
|
+
type Props = { todo: Todo }
|
|
3918
|
+
export function TodoItem(props: Props) {
|
|
3919
|
+
return <li>{props.todo.text}</li>
|
|
3920
|
+
}
|
|
3921
|
+
`
|
|
3922
|
+
|
|
3923
|
+
test('emits .Todo.Done, not bare .Done, in the loop-gating {{if}}', () => {
|
|
3924
|
+
const adapter = new GoTemplateAdapter()
|
|
3925
|
+
const ir = compileToIR(TODO_FILTER_PROBE_SOURCE, adapter)
|
|
3926
|
+
const { template } = adapter.generate(ir)
|
|
3927
|
+
|
|
3928
|
+
// The datum-carrying field is derived from the prop that receives the
|
|
3929
|
+
// loop param verbatim (`todo={todo}` → `Todo`, `capitalizeFieldName('todo')`
|
|
3930
|
+
// — the SAME derivation `generatePropsStruct` uses for every other
|
|
3931
|
+
// prop-to-field mapping), not hardcoded.
|
|
3932
|
+
expect(template).toContain('not .Todo.Done')
|
|
3933
|
+
expect(template).toContain('(.Todo.Done)')
|
|
3934
|
+
// The bare (unqualified) form must not survive the fix — this is the
|
|
3935
|
+
// literal string `html/template` failed to resolve pre-fix
|
|
3936
|
+
// (`can't evaluate field Done in type TodoItemProps`).
|
|
3937
|
+
expect(template).not.toContain('not .Done')
|
|
3938
|
+
expect(template).not.toContain('(.Done)')
|
|
3939
|
+
// Still ranges the wrapper slice (#2130's retarget is untouched).
|
|
3940
|
+
expect(template).toContain(':= .TodoItems}}')
|
|
3941
|
+
})
|
|
3942
|
+
|
|
3943
|
+
test('real `go run`: default filter "active" renders only the not-done item', async () => {
|
|
3944
|
+
let html: string
|
|
3945
|
+
try {
|
|
3946
|
+
html = await renderGoTemplateComponent({
|
|
3947
|
+
source: TODO_FILTER_PROBE_SOURCE.trimStart(),
|
|
3948
|
+
adapter: new GoTemplateAdapter(),
|
|
3949
|
+
components: { './todo-item': TODO_ITEM_SOURCE.trimStart() },
|
|
3950
|
+
props: {
|
|
3951
|
+
initialTodos: [
|
|
3952
|
+
{ id: 1, text: 'Eat breakfast', done: true },
|
|
3953
|
+
{ id: 2, text: 'Write tests', done: false },
|
|
3954
|
+
],
|
|
3955
|
+
},
|
|
3956
|
+
})
|
|
3957
|
+
} catch (err) {
|
|
3958
|
+
if (err instanceof GoNotAvailableError) {
|
|
3959
|
+
console.log('Skipping #2228 filter-predicate e2e: go command not found')
|
|
3960
|
+
return
|
|
3961
|
+
}
|
|
3962
|
+
throw err
|
|
3963
|
+
}
|
|
3964
|
+
// Pre-fix this either 500s at `tmpl.ExecuteTemplate` (`can't evaluate
|
|
3965
|
+
// field Done in type TodoItemProps`) or — since `bf_sort_eval`-style
|
|
3966
|
+
// evaluators are untouched by this fix, only the html/template dot-path
|
|
3967
|
+
// is — renders wrong. Post-fix: only the not-done todo (id 2) survives
|
|
3968
|
+
// the 'active' filter.
|
|
3969
|
+
expect(html).not.toContain('Eat breakfast')
|
|
3970
|
+
expect(html).toContain('Write tests')
|
|
3971
|
+
expect(html).toContain('data-key="2"')
|
|
3972
|
+
expect(html).not.toContain('data-key="1"')
|
|
3973
|
+
})
|
|
3974
|
+
})
|
|
3975
|
+
|
|
3976
|
+
// #2208: a static-array loop whose body is a single child component with a
|
|
3977
|
+
// plain-value prop set bakes the per-item props/data-key directly into the
|
|
3978
|
+
// generated constructor — see `analyzeBakeableStaticChildLoop`
|
|
3979
|
+
// (`analysis/static-child-loop-bake.ts`). Two-file fixture shape (sibling
|
|
3980
|
+
// `list-item.tsx`, `siblingTemplatesRegistered: true`) since Go's own BF103
|
|
3981
|
+
// cross-template-registration check (independent of #2208) would otherwise
|
|
3982
|
+
// fire first — mirrors `jsx-runner.ts`'s `compileWithDiagnostics`.
|
|
3983
|
+
describe('GoTemplateAdapter - static array-of-objects loop source baking (#2208)', () => {
|
|
3984
|
+
const STATIC_LIST_SOURCE = `
|
|
3985
|
+
import { ListItem } from './list-item'
|
|
3986
|
+
export function StaticList() {
|
|
3987
|
+
const items = [{ label: 'Alpha' }, { label: 'Beta' }]
|
|
3988
|
+
return (
|
|
3989
|
+
<ul>
|
|
3990
|
+
{items.map(item => (
|
|
3991
|
+
<ListItem key={item.label} label={item.label} className="text-sm" />
|
|
3992
|
+
))}
|
|
3993
|
+
</ul>
|
|
3994
|
+
)
|
|
3995
|
+
}
|
|
3996
|
+
`
|
|
3997
|
+
|
|
3998
|
+
function compileStaticList(adapter?: GoTemplateAdapter) {
|
|
3999
|
+
return compileJSX(STATIC_LIST_SOURCE, 'test.tsx', {
|
|
4000
|
+
adapter: adapter ?? new GoTemplateAdapter(),
|
|
4001
|
+
siblingTemplatesRegistered: true,
|
|
4002
|
+
outputIR: false,
|
|
4003
|
+
})
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
test('compiles with no BF101 (no longer refused)', () => {
|
|
4007
|
+
const result = compileStaticList()
|
|
4008
|
+
expect(result.errors ?? []).toEqual([])
|
|
4009
|
+
})
|
|
4010
|
+
|
|
4011
|
+
test('constructor bakes each item\'s props and data-key directly', () => {
|
|
4012
|
+
const result = compileStaticList()
|
|
4013
|
+
const types = result.files.find(f => f.type === 'types')!.content
|
|
4014
|
+
expect(types).toContain('NewListItemProps(ListItemInput{Label: "Alpha", ClassName: "text-sm"})')
|
|
4015
|
+
expect(types).toContain('NewListItemProps(ListItemInput{Label: "Beta", ClassName: "text-sm"})')
|
|
4016
|
+
expect(types).toContain('BfDataKey = "Alpha"')
|
|
4017
|
+
expect(types).toContain('BfDataKey = "Beta"')
|
|
4018
|
+
})
|
|
4019
|
+
|
|
4020
|
+
test('the Input struct carries no ListItems field (nothing for a caller to supply)', () => {
|
|
4021
|
+
const result = compileStaticList()
|
|
4022
|
+
const types = result.files.find(f => f.type === 'types')!.content
|
|
4023
|
+
const inputStruct = types.slice(types.indexOf('StaticListInput struct'), types.indexOf('StaticListProps struct'))
|
|
4024
|
+
expect(inputStruct).not.toContain('ListItems')
|
|
4025
|
+
})
|
|
4026
|
+
|
|
4027
|
+
test('the template still ranges over .ListItems (unchanged)', () => {
|
|
4028
|
+
const result = compileStaticList()
|
|
4029
|
+
const template = result.files.find(f => f.type === 'markedTemplate')!.content
|
|
4030
|
+
expect(template).toContain(':= .ListItems}}')
|
|
4031
|
+
})
|
|
4032
|
+
|
|
4033
|
+
test('a runtime-computed const (#2069) still refuses with BF101', () => {
|
|
4034
|
+
const result = compileJSX(
|
|
4035
|
+
`
|
|
4036
|
+
import { ListItem } from './list-item'
|
|
4037
|
+
export function TagList(props: { tags: string[] }) {
|
|
4038
|
+
const entries = props.tags.filter(t => t !== '')
|
|
4039
|
+
return (
|
|
4040
|
+
<ul>
|
|
4041
|
+
{entries.map(label => (
|
|
4042
|
+
<ListItem key={label} label={label} />
|
|
4043
|
+
))}
|
|
4044
|
+
</ul>
|
|
4045
|
+
)
|
|
4046
|
+
}
|
|
4047
|
+
`,
|
|
4048
|
+
'test.tsx',
|
|
4049
|
+
{ adapter: new GoTemplateAdapter(), siblingTemplatesRegistered: true },
|
|
4050
|
+
)
|
|
4051
|
+
expect(result.errors?.some(e => e.code === 'BF101')).toBe(true)
|
|
4052
|
+
})
|
|
4053
|
+
|
|
4054
|
+
// Fable review: `bf build` compiles a whole source dir through ONE reused
|
|
4055
|
+
// adapter instance (loop marker ids restart at `l0` per component) — the
|
|
4056
|
+
// bakeability cache must reset every `generate()` or a stale entry from a
|
|
4057
|
+
// PREVIOUS component either silently suppresses this fix, or (worse)
|
|
4058
|
+
// leaks that other component's baked literal values into this one.
|
|
4059
|
+
const TAG_LIST_SOURCE = `
|
|
4060
|
+
import { ListItem } from './list-item'
|
|
4061
|
+
export function TagList(props: { tags: string[] }) {
|
|
4062
|
+
const entries = props.tags.filter(t => t !== '')
|
|
4063
|
+
return (
|
|
4064
|
+
<ul>
|
|
4065
|
+
{entries.map(label => (
|
|
4066
|
+
<ListItem key={label} label={label} />
|
|
4067
|
+
))}
|
|
4068
|
+
</ul>
|
|
4069
|
+
)
|
|
4070
|
+
}
|
|
4071
|
+
`
|
|
4072
|
+
|
|
4073
|
+
test('a reused adapter does not suppress baking for a later component sharing a marker id', () => {
|
|
4074
|
+
const adapter = new GoTemplateAdapter()
|
|
4075
|
+
compileJSX(TAG_LIST_SOURCE, 'test.tsx', { adapter, siblingTemplatesRegistered: true })
|
|
4076
|
+
const second = compileJSX(STATIC_LIST_SOURCE, 'test.tsx', { adapter, siblingTemplatesRegistered: true })
|
|
4077
|
+
expect(second.errors ?? []).toEqual([])
|
|
4078
|
+
const types = second.files.find(f => f.type === 'types')!.content
|
|
4079
|
+
expect(types).toContain('NewListItemProps(ListItemInput{Label: "Alpha"')
|
|
4080
|
+
})
|
|
4081
|
+
|
|
4082
|
+
test('a reused adapter does not leak a prior component\'s baked data into a later runtime-computed one', () => {
|
|
4083
|
+
const adapter = new GoTemplateAdapter()
|
|
4084
|
+
compileJSX(STATIC_LIST_SOURCE, 'test.tsx', { adapter, siblingTemplatesRegistered: true })
|
|
4085
|
+
const second = compileJSX(TAG_LIST_SOURCE, 'test.tsx', { adapter, siblingTemplatesRegistered: true })
|
|
4086
|
+
expect(second.errors?.some(e => e.code === 'BF101')).toBe(true)
|
|
4087
|
+
const types = second.files.find(f => f.type === 'types')!.content
|
|
4088
|
+
expect(types).not.toContain('"Alpha"')
|
|
4089
|
+
})
|
|
4090
|
+
|
|
4091
|
+
// Fable re-review: `generateTypes()` is ALSO a standalone public entry
|
|
4092
|
+
// point (the Go conformance harness in `test-render.ts` calls it
|
|
4093
|
+
// directly on an already-`generate()`d adapter for a sibling/child IR),
|
|
4094
|
+
// not just an internal step of `generate()` — the bake cache must reset
|
|
4095
|
+
// there too, or a marker id collision with a PREVIOUS `generate()` call
|
|
4096
|
+
// leaks that other component's baked data through this door instead.
|
|
4097
|
+
test('a standalone generateTypes() call does not leak a prior generate() pass\'s baked data', () => {
|
|
4098
|
+
const adapter = new GoTemplateAdapter()
|
|
4099
|
+
const tagListIR = compileToIR(TAG_LIST_SOURCE, adapter)
|
|
4100
|
+
adapter.generate(tagListIR, { siblingTemplatesRegistered: true })
|
|
4101
|
+
const staticListIR = compileToIR(STATIC_LIST_SOURCE, adapter)
|
|
4102
|
+
adapter.generate(staticListIR, { siblingTemplatesRegistered: true })
|
|
4103
|
+
|
|
4104
|
+
const types = adapter.generateTypes(tagListIR)
|
|
4105
|
+
expect(types).not.toContain('"Alpha"')
|
|
4106
|
+
expect(types).toContain('ListItems []ListItemInput')
|
|
4107
|
+
})
|
|
4108
|
+
|
|
4109
|
+
// Fable review: a static loop-SOURCE identifier must not resolve through
|
|
4110
|
+
// an outer const when a DIFFERENT, enclosing loop's own callback param
|
|
4111
|
+
// shadows that same name.
|
|
4112
|
+
test('a static const shadowed by an enclosing loop param does not get baked', () => {
|
|
4113
|
+
const result = compileJSX(
|
|
4114
|
+
`
|
|
4115
|
+
import { ListItem } from './list-item'
|
|
4116
|
+
export function Nested({ groups }: { groups: { label: string }[][] }) {
|
|
4117
|
+
const items = [{ label: 'Alpha' }, { label: 'Beta' }]
|
|
4118
|
+
return (
|
|
4119
|
+
<div>
|
|
4120
|
+
{groups.map((items, i) => (
|
|
4121
|
+
<ul key={i}>
|
|
4122
|
+
{items.map(item => (
|
|
4123
|
+
<ListItem key={item.label} label={item.label} />
|
|
4124
|
+
))}
|
|
4125
|
+
</ul>
|
|
4126
|
+
))}
|
|
4127
|
+
</div>
|
|
4128
|
+
)
|
|
4129
|
+
}
|
|
4130
|
+
`,
|
|
4131
|
+
'test.tsx',
|
|
4132
|
+
{ adapter: new GoTemplateAdapter(), siblingTemplatesRegistered: true },
|
|
4133
|
+
)
|
|
4134
|
+
const types = result.files.find(f => f.type === 'types')?.content ?? ''
|
|
4135
|
+
expect(types).not.toContain('"Alpha"')
|
|
4136
|
+
})
|
|
4137
|
+
})
|
|
4138
|
+
|
|
4139
|
+
// #2224: the two shapes #2208 deliberately left refused.
|
|
4140
|
+
// Shape 1 — a static-array loop whose body is a PLAIN ELEMENT (no child
|
|
4141
|
+
// component): unrolled once per item at template-generation time (no Go
|
|
4142
|
+
// struct synthesis, no `{{range}}`) — see
|
|
4143
|
+
// `analysis/static-element-loop-bake.ts`'s docstring for the exact gate.
|
|
4144
|
+
// Shape 2 — an INLINE, unnamed array literal directly in `.map()`, with
|
|
4145
|
+
// either body kind — routed into shape 1's unroll (element body) or
|
|
4146
|
+
// #2208's existing bake (component body) the same way a named const is.
|
|
4147
|
+
describe('GoTemplateAdapter - static array-of-objects loop, plain-element body + inline literal (#2224)', () => {
|
|
4148
|
+
test('shape 1: named const + plain-element body compiles with no BF101', () => {
|
|
4149
|
+
const result = compileJSX(
|
|
4150
|
+
`
|
|
4151
|
+
export function List() {
|
|
4152
|
+
const items = [{ label: 'Alpha' }, { label: 'Beta' }]
|
|
4153
|
+
return <ul>{items.map(item => <li key={item.label}>{item.label}</li>)}</ul>
|
|
4154
|
+
}
|
|
4155
|
+
`,
|
|
4156
|
+
'test.tsx',
|
|
4157
|
+
{ adapter: new GoTemplateAdapter() },
|
|
4158
|
+
)
|
|
4159
|
+
expect(result.errors ?? []).toEqual([])
|
|
4160
|
+
})
|
|
4161
|
+
|
|
4162
|
+
test('shape 1: the template is unrolled once per item, no {{range}}', () => {
|
|
4163
|
+
const result = compileJSX(
|
|
4164
|
+
`
|
|
4165
|
+
export function List() {
|
|
4166
|
+
const items = [{ label: 'Alpha' }, { label: 'Beta' }]
|
|
4167
|
+
return <ul>{items.map(item => <li key={item.label}>{item.label}</li>)}</ul>
|
|
4168
|
+
}
|
|
4169
|
+
`,
|
|
4170
|
+
'test.tsx',
|
|
4171
|
+
{ adapter: new GoTemplateAdapter() },
|
|
4172
|
+
)
|
|
4173
|
+
const template = result.files.find(f => f.type === 'markedTemplate')!.content
|
|
4174
|
+
expect(template).not.toContain('{{range')
|
|
4175
|
+
// The `<!--bf-loop:id--> ... <!--/bf-loop:id-->` marker pair a dynamic
|
|
4176
|
+
// loop emits still wraps the unrolled body, so the CSR-side static
|
|
4177
|
+
// `forEach` wiring (a separate, unaffected compiler pass) finds the
|
|
4178
|
+
// same DOM range.
|
|
4179
|
+
expect(template).toContain('{{bfComment "loop:l0"}}')
|
|
4180
|
+
expect(template).toContain('{{bfComment "/loop:l0"}}')
|
|
4181
|
+
// Per item: the SAME `data-key` / `bfTextStart`/`bfTextEnd` markers a
|
|
4182
|
+
// dynamic `{{range}}` over `.Field` would emit, with the item's value
|
|
4183
|
+
// substituted as a literal Go string instead of a `.Field` reference.
|
|
4184
|
+
expect(template).toContain('<li data-key="{{"Alpha"}}">{{bfTextStart "s0"}}{{"Alpha"}}{{bfTextEnd}}</li>')
|
|
4185
|
+
expect(template).toContain('<li data-key="{{"Beta"}}">{{bfTextStart "s0"}}{{"Beta"}}{{bfTextEnd}}</li>')
|
|
4186
|
+
})
|
|
4187
|
+
|
|
4188
|
+
test('shape 1 rendered through real `go run` produces the expected HTML', async () => {
|
|
4189
|
+
try {
|
|
4190
|
+
const html = await renderGoTemplateComponent({
|
|
4191
|
+
source: `
|
|
4192
|
+
export function List() {
|
|
4193
|
+
const items = [{ label: 'Alpha' }, { label: 'Beta' }]
|
|
4194
|
+
return <ul>{items.map(item => <li key={item.label}>{item.label}</li>)}</ul>
|
|
4195
|
+
}
|
|
4196
|
+
`,
|
|
4197
|
+
adapter: new GoTemplateAdapter(),
|
|
4198
|
+
props: {},
|
|
4199
|
+
})
|
|
4200
|
+
expect(html).toContain('<li data-key="Alpha"><!--bf:s0-->Alpha<!--/--></li>')
|
|
4201
|
+
expect(html).toContain('<li data-key="Beta"><!--bf:s0-->Beta<!--/--></li>')
|
|
4202
|
+
} catch (err) {
|
|
4203
|
+
if (err instanceof GoNotAvailableError) {
|
|
4204
|
+
console.log('Skipping #2224 shape-1 e2e: go command not found')
|
|
4205
|
+
return
|
|
4206
|
+
}
|
|
4207
|
+
throw err
|
|
4208
|
+
}
|
|
4209
|
+
})
|
|
4210
|
+
|
|
4211
|
+
test('shape 2, element body: inline unnamed array literal compiles and unrolls the same as a named const', () => {
|
|
4212
|
+
const result = compileJSX(
|
|
4213
|
+
`
|
|
4214
|
+
export function List() {
|
|
4215
|
+
return <ul>{[{ label: 'Alpha' }, { label: 'Beta' }].map(item => <li key={item.label}>{item.label}</li>)}</ul>
|
|
4216
|
+
}
|
|
4217
|
+
`,
|
|
4218
|
+
'test.tsx',
|
|
4219
|
+
{ adapter: new GoTemplateAdapter() },
|
|
4220
|
+
)
|
|
4221
|
+
expect(result.errors ?? []).toEqual([])
|
|
4222
|
+
const template = result.files.find(f => f.type === 'markedTemplate')!.content
|
|
4223
|
+
expect(template).not.toContain('{{range')
|
|
4224
|
+
expect(template).toContain('{{"Alpha"}}')
|
|
4225
|
+
expect(template).toContain('{{"Beta"}}')
|
|
4226
|
+
})
|
|
4227
|
+
|
|
4228
|
+
test('shape 2, component body: inline unnamed array literal reaches the #2208 constructor bake (previously BF101 "Expression not supported")', () => {
|
|
4229
|
+
const result = compileJSX(
|
|
4230
|
+
`
|
|
4231
|
+
import { ListItem } from './list-item'
|
|
4232
|
+
export function List() {
|
|
4233
|
+
return (
|
|
4234
|
+
<ul>
|
|
4235
|
+
{[{ label: 'Alpha' }, { label: 'Beta' }].map(item => (
|
|
4236
|
+
<ListItem key={item.label} label={item.label} />
|
|
4237
|
+
))}
|
|
4238
|
+
</ul>
|
|
4239
|
+
)
|
|
4240
|
+
}
|
|
4241
|
+
`,
|
|
4242
|
+
'test.tsx',
|
|
4243
|
+
{ adapter: new GoTemplateAdapter(), siblingTemplatesRegistered: true },
|
|
4244
|
+
)
|
|
4245
|
+
expect(result.errors ?? []).toEqual([])
|
|
4246
|
+
const types = result.files.find(f => f.type === 'types')!.content
|
|
4247
|
+
expect(types).toContain('NewListItemProps(ListItemInput{Label: "Alpha"})')
|
|
4248
|
+
expect(types).toContain('NewListItemProps(ListItemInput{Label: "Beta"})')
|
|
4249
|
+
// The template still ranges over `.ListItems` (#2208's shape — only the
|
|
4250
|
+
// constructor's data is baked, not the range itself).
|
|
4251
|
+
const template = result.files.find(f => f.type === 'markedTemplate')!.content
|
|
4252
|
+
expect(template).toContain(':= .ListItems}}')
|
|
4253
|
+
})
|
|
4254
|
+
|
|
4255
|
+
test('gate: a body expression that cannot fold against the item (a signal-shaped call) keeps the BF101 refusal', () => {
|
|
4256
|
+
const result = compileJSX(
|
|
4257
|
+
`
|
|
4258
|
+
export function List() {
|
|
4259
|
+
const items = [{ label: 'Alpha' }, { label: 'Beta' }]
|
|
4260
|
+
return <ul>{items.map(item => <li key={item.label}>{item.label} - {Date.now()}</li>)}</ul>
|
|
4261
|
+
}
|
|
4262
|
+
`,
|
|
4263
|
+
'test.tsx',
|
|
4264
|
+
{ adapter: new GoTemplateAdapter() },
|
|
4265
|
+
)
|
|
4266
|
+
expect(result.errors?.some(e => e.code === 'BF101')).toBe(true)
|
|
4267
|
+
const template = result.files.find(f => f.type === 'markedTemplate')?.content ?? ''
|
|
4268
|
+
expect(template).not.toContain('{{"Alpha"}}')
|
|
4269
|
+
})
|
|
4270
|
+
|
|
4271
|
+
test('gate: an index-param reference keeps the BF101 refusal (index is deliberately not folded)', () => {
|
|
4272
|
+
const result = compileJSX(
|
|
4273
|
+
`
|
|
4274
|
+
export function List() {
|
|
4275
|
+
const items = [{ label: 'Alpha' }, { label: 'Beta' }]
|
|
4276
|
+
return <ul>{items.map((item, i) => <li key={item.label}>{i}: {item.label}</li>)}</ul>
|
|
4277
|
+
}
|
|
4278
|
+
`,
|
|
4279
|
+
'test.tsx',
|
|
4280
|
+
{ adapter: new GoTemplateAdapter() },
|
|
4281
|
+
)
|
|
4282
|
+
expect(result.errors?.some(e => e.code === 'BF101')).toBe(true)
|
|
4283
|
+
})
|
|
4284
|
+
|
|
4285
|
+
test('gate: a nested loop inside the body keeps the BF101 refusal', () => {
|
|
4286
|
+
const result = compileJSX(
|
|
4287
|
+
`
|
|
4288
|
+
export function List() {
|
|
4289
|
+
const items = [{ label: 'Alpha', tags: ['x', 'y'] }]
|
|
4290
|
+
return <ul>{items.map(item => <li key={item.label}>{item.tags.map(t => <span>{t}</span>)}</li>)}</ul>
|
|
4291
|
+
}
|
|
4292
|
+
`,
|
|
4293
|
+
'test.tsx',
|
|
4294
|
+
{ adapter: new GoTemplateAdapter() },
|
|
4295
|
+
)
|
|
4296
|
+
expect(result.errors?.some(e => e.code === 'BF101')).toBe(true)
|
|
4297
|
+
})
|
|
4298
|
+
|
|
4299
|
+
test('gate: a non-scalar item field (array/object) keeps the BF101 refusal', () => {
|
|
4300
|
+
const result = compileJSX(
|
|
4301
|
+
`
|
|
4302
|
+
export function List() {
|
|
4303
|
+
const items = [{ label: 'Alpha', tags: ['x'] }]
|
|
4304
|
+
return <ul>{items.map(item => <li key={item.label}>{item.tags}</li>)}</ul>
|
|
4305
|
+
}
|
|
4306
|
+
`,
|
|
4307
|
+
'test.tsx',
|
|
4308
|
+
{ adapter: new GoTemplateAdapter() },
|
|
4309
|
+
)
|
|
4310
|
+
expect(result.errors?.some(e => e.code === 'BF101')).toBe(true)
|
|
4311
|
+
})
|
|
4312
|
+
|
|
4313
|
+
test('gate: .flatMap() keeps the BF101 refusal (out of scope)', () => {
|
|
4314
|
+
const result = compileJSX(
|
|
4315
|
+
`
|
|
4316
|
+
export function List() {
|
|
4317
|
+
const items = [{ label: 'Alpha' }, { label: 'Beta' }]
|
|
4318
|
+
return <ul>{items.flatMap(item => [<li key={item.label}>{item.label}</li>])}</ul>
|
|
4319
|
+
}
|
|
4320
|
+
`,
|
|
4321
|
+
'test.tsx',
|
|
4322
|
+
{ adapter: new GoTemplateAdapter() },
|
|
4323
|
+
)
|
|
4324
|
+
expect(result.errors?.some(e => e.code === 'BF101')).toBe(true)
|
|
4325
|
+
})
|
|
4326
|
+
|
|
4327
|
+
test('a static const shadowed by an enclosing loop param does not get unrolled (element body)', () => {
|
|
4328
|
+
const result = compileJSX(
|
|
4329
|
+
`
|
|
4330
|
+
export function Nested({ groups }: { groups: { label: string }[][] }) {
|
|
4331
|
+
const items = [{ label: 'Alpha' }, { label: 'Beta' }]
|
|
4332
|
+
return (
|
|
4333
|
+
<div>
|
|
4334
|
+
{groups.map((items, i) => (
|
|
4335
|
+
<ul key={i}>
|
|
4336
|
+
{items.map(item => (
|
|
4337
|
+
<li key={item.label}>{item.label}</li>
|
|
4338
|
+
))}
|
|
4339
|
+
</ul>
|
|
4340
|
+
))}
|
|
4341
|
+
</div>
|
|
4342
|
+
)
|
|
4343
|
+
}
|
|
4344
|
+
`,
|
|
4345
|
+
'test.tsx',
|
|
4346
|
+
{ adapter: new GoTemplateAdapter() },
|
|
4347
|
+
)
|
|
4348
|
+
const template = result.files.find(f => f.type === 'markedTemplate')?.content ?? ''
|
|
4349
|
+
expect(template).not.toContain('{{"Alpha"}}')
|
|
4350
|
+
})
|
|
4351
|
+
})
|
|
4352
|
+
|
|
4353
|
+
// #2236: two independent loop-param-shadowing gaps, both distinct from the
|
|
4354
|
+
// #2224 static-array unroll above (this describe exercises the DYNAMIC
|
|
4355
|
+
// (signal-driven) `{{range}}` path, where a real per-iteration `.` context
|
|
4356
|
+
// exists — #2224's baked/unrolled loops are a different code path entirely).
|
|
4357
|
+
//
|
|
4358
|
+
// Slice A: `convertExpressionToGo`'s bare-identifier fast path (the
|
|
4359
|
+
// "inline a function-scope literal const" shortcut, e.g. `totalPages`) is a
|
|
4360
|
+
// STRING-KEYED check over the raw JS source text reached directly by call
|
|
4361
|
+
// sites like attribute emission (`key={count}` → `data-key`) — it never
|
|
4362
|
+
// goes through `identifier()` (the `ParsedExprEmitter` method), which
|
|
4363
|
+
// already carries the loop-shadow guards (`loopParamStack` /
|
|
4364
|
+
// `isOuterLoopParam`, mirrored from `resolveModuleStringConst` /
|
|
4365
|
+
// `resolveModuleNumericConst`). So a `.map((count) => ...)` callback param
|
|
4366
|
+
// that shadows an outer `const count = 7` got the OUTER literal inlined at
|
|
4367
|
+
// the `data-key` position even though the text position (which DOES go
|
|
4368
|
+
// through `identifier()`) correctly resolved to the per-item value.
|
|
4369
|
+
//
|
|
4370
|
+
// Slice B: Go's `collectStringValueNames` (prop-classes.ts) was ported from
|
|
4371
|
+
// Blade BEFORE #2212 added the local-const inclusion + `collectLoopBoundNames`
|
|
4372
|
+
// exclusion, so an outer string-typed prop/signal whose name is shadowed by a
|
|
4373
|
+
// `.map()` callback param still poisoned the shadowed occurrence's type
|
|
4374
|
+
// resolution — `1 + label` inside `values.map((label) => ...)` (with an outer
|
|
4375
|
+
// `label: string` prop) emitted `bf_concat_str` (string concat) instead of
|
|
4376
|
+
// `bf_add` (numeric addition). The full #2212 shape is ported: same-file
|
|
4377
|
+
// local consts join the set (so an outer `{label + suffix}` with
|
|
4378
|
+
// `suffix = '!'` still classifies as concat via its OTHER operand once
|
|
4379
|
+
// `label` is subtracted — the exact `loop-param-shadows-outer-name` fixture
|
|
4380
|
+
// shape) and loop-bound names are excluded.
|
|
4381
|
+
describe('GoTemplateAdapter - const/type resolution vs loop-param shadowing (#2236)', () => {
|
|
4382
|
+
test('slice A: data-key inside a dynamic (signal-driven) loop uses the loop value, not the outer const', () => {
|
|
4383
|
+
const { template } = compileAndGenerate(`
|
|
4384
|
+
'use client'
|
|
4385
|
+
import { createSignal } from '@barefootjs/client'
|
|
4386
|
+
export function List() {
|
|
4387
|
+
const count = 7
|
|
4388
|
+
const [nums] = createSignal<number[]>([2, 5])
|
|
4389
|
+
return <ul>{nums().map((count) => <li key={count}>{count * 3}</li>)}</ul>
|
|
4390
|
+
}
|
|
4391
|
+
`)
|
|
4392
|
+
// The range establishes a real per-iteration dot context — both the
|
|
4393
|
+
// `data-key` attribute and the text position must resolve the shadowed
|
|
4394
|
+
// `count` through it.
|
|
4395
|
+
expect(template).toContain('{{range $_, $count := .Nums}}<li data-key="{{.}}">')
|
|
4396
|
+
expect(template).toContain('{{bf_mul . 3}}')
|
|
4397
|
+
// Never the outer `const count = 7` literal.
|
|
4398
|
+
expect(template).not.toContain('{{7}}')
|
|
4399
|
+
})
|
|
4400
|
+
|
|
4401
|
+
test('slice A: a DESTRUCTURED callback binding shadowing an outer const resolves to the binding accessor (#2242 Copilot review)', () => {
|
|
4402
|
+
// Destructured callbacks push '' onto loopParamStack and track their
|
|
4403
|
+
// binding names only in loopBindingStack — the fast-path guard must
|
|
4404
|
+
// scan that stack too, or the outer `const id = 7` inlines at both
|
|
4405
|
+
// the key and text positions.
|
|
4406
|
+
const { template } = compileAndGenerate(`
|
|
4407
|
+
'use client'
|
|
4408
|
+
import { createSignal } from '@barefootjs/client'
|
|
4409
|
+
export function List() {
|
|
4410
|
+
const id = 7
|
|
4411
|
+
const [items] = createSignal<{ id: number }[]>([{ id: 2 }, { id: 5 }])
|
|
4412
|
+
return <ul>{items().map(({ id }) => <li key={id}>{id}</li>)}</ul>
|
|
4413
|
+
}
|
|
4414
|
+
`)
|
|
4415
|
+
expect(template).toContain('data-key="{{$__bf_item0.ID}}"')
|
|
4416
|
+
expect(template).toContain('{{$__bf_item0.ID}}{{bfTextEnd}}')
|
|
4417
|
+
expect(template).not.toContain('{{7}}')
|
|
4418
|
+
})
|
|
4419
|
+
|
|
4420
|
+
test('record-member fast path: a module object const shadowed by the callback param resolves per-item (loop-param-shadows-record-const fixture)', () => {
|
|
4421
|
+
// resolveStaticRecordLiteralIndex covers IDENT['key'] AND IDENT.key —
|
|
4422
|
+
// the record-member sibling of the bare-identifier gap above. Without
|
|
4423
|
+
// the isLoopShadowedName guard it baked {{"outer-lit"}} into every
|
|
4424
|
+
// iteration.
|
|
4425
|
+
const { template } = compileAndGenerate(`
|
|
4426
|
+
const cfg = { x: 'outer-lit' }
|
|
4427
|
+
export function List({ rows }: { rows: { id: number; x: string }[] }) {
|
|
4428
|
+
return <ul>{rows.map((cfg) => <li key={cfg.id}>{cfg.x}</li>)}</ul>
|
|
4429
|
+
}
|
|
4430
|
+
`)
|
|
4431
|
+
expect(template).toContain('{{range $_, $cfg := .Rows}}')
|
|
4432
|
+
expect(template).toContain('{{.X}}')
|
|
4433
|
+
expect(template).not.toContain('outer-lit')
|
|
4434
|
+
})
|
|
4435
|
+
|
|
4436
|
+
test('slice B: `1 + label` inside the loop that shadows an outer string prop lowers to bf_add, not bf_concat_str', () => {
|
|
4437
|
+
const { template } = compileAndGenerate(`
|
|
4438
|
+
'use client'
|
|
4439
|
+
export function Labels({ label, values }: { label: string; values: number[] }) {
|
|
4440
|
+
return <ul>{values.map((label) => <li key={label}>{1 + label}</li>)}</ul>
|
|
4441
|
+
}
|
|
4442
|
+
`)
|
|
4443
|
+
expect(template).toContain('{{bf_add 1 .}}')
|
|
4444
|
+
expect(template).not.toContain('bf_concat_str')
|
|
4445
|
+
})
|
|
4446
|
+
|
|
4447
|
+
test('regression pin: a const inlined OUTSIDE any loop still inlines (slice A must not over-guard)', () => {
|
|
4448
|
+
const { template } = compileAndGenerate(`
|
|
4449
|
+
export function Foo() {
|
|
4450
|
+
const total = 5
|
|
4451
|
+
return <div data-key={total}>{total}</div>
|
|
4452
|
+
}
|
|
4453
|
+
`)
|
|
4454
|
+
expect(template).toContain('data-key="{{5}}"')
|
|
4455
|
+
expect(template).toContain('{{5}}</div>')
|
|
4456
|
+
})
|
|
4457
|
+
|
|
4458
|
+
test('regression pin: a genuinely string-typed `+` OUTSIDE the loop still lowers to bf_concat_str (slice B must not over-guard)', () => {
|
|
4459
|
+
const { template } = compileAndGenerate(`
|
|
4460
|
+
export function Foo({ label }: { label: string }) {
|
|
4461
|
+
const suffix = '!'
|
|
4462
|
+
return <p>{label + suffix}</p>
|
|
4463
|
+
}
|
|
4464
|
+
`)
|
|
4465
|
+
expect(template).toContain('bf_concat_str .Label .Suffix')
|
|
4466
|
+
})
|
|
4467
|
+
|
|
4468
|
+
test('local-const operand carries the concat classification when the prop operand is coarsely excluded (fixture shape)', () => {
|
|
4469
|
+
// The `loop-param-shadows-outer-name` fixture's combined shape: `label`
|
|
4470
|
+
// is loop-bound below, so the coarse exclusion strips it from the string
|
|
4471
|
+
// set — the OUTER `{label + suffix}` must then classify as concat via
|
|
4472
|
+
// its `suffix = '!'` local-const operand (the #2212 local-const
|
|
4473
|
+
// inclusion), or it would fall back to `bf_add` and render `0`.
|
|
4474
|
+
const { template } = compileAndGenerate(`
|
|
4475
|
+
'use client'
|
|
4476
|
+
import { createSignal } from '@barefootjs/client'
|
|
4477
|
+
export function Both({ label, values }: { label: string; values: number[] }) {
|
|
4478
|
+
const suffix = '!'
|
|
4479
|
+
const [n, setN] = createSignal(0)
|
|
4480
|
+
return (
|
|
4481
|
+
<div data-n={n()} onClick={() => setN(n() + 1)}>
|
|
4482
|
+
<p>{label + suffix}</p>
|
|
4483
|
+
<ul>{values.map((label) => <li key={label}>{1 + label}</li>)}</ul>
|
|
4484
|
+
</div>
|
|
4485
|
+
)
|
|
4486
|
+
}
|
|
4487
|
+
`)
|
|
4488
|
+
// Outside the loop: still string concat, carried by the const operand.
|
|
4489
|
+
expect(template).toContain('bf_concat_str .Label .Suffix')
|
|
4490
|
+
// Inside the loop: the shadowed occurrence stays numeric.
|
|
4491
|
+
expect(template).toContain('{{bf_add 1 .}}')
|
|
4492
|
+
})
|
|
4493
|
+
|
|
4494
|
+
// Go's slice-A guard is scope-PRECISE: it consults the live
|
|
4495
|
+
// `loopParamStack`, not a flat component-wide name set, so a const whose
|
|
4496
|
+
// name is loop-bound ELSEWHERE in the component still inlines at an
|
|
4497
|
+
// occurrence that is genuinely outside any loop. This is a real point of
|
|
4498
|
+
// divergence from the Twig-family adapters' coarse `collectLoopBoundNames`
|
|
4499
|
+
// trade-off — pinned here so a future "fix" doesn't accidentally coarsen
|
|
4500
|
+
// Go's precise guard to match them.
|
|
4501
|
+
test('slice A guard is scope-precise: a name that is loop-bound elsewhere still inlines outside the loop', () => {
|
|
4502
|
+
const { template } = compileAndGenerate(`
|
|
4503
|
+
export function Foo({ items }: { items: number[] }) {
|
|
4504
|
+
const count = 9
|
|
4505
|
+
return <div>
|
|
4506
|
+
<p data-key={count}>{count}</p>
|
|
4507
|
+
<ul>{items.map(count => <li key={count}>{count}</li>)}</ul>
|
|
4508
|
+
</div>
|
|
4509
|
+
}
|
|
4510
|
+
`)
|
|
4511
|
+
// Outside the loop, `count` still resolves to the outer literal `9`.
|
|
4512
|
+
expect(template).toContain('data-key="{{9}}"')
|
|
4513
|
+
// Inside the loop, the shadowed occurrence still resolves to the loop
|
|
4514
|
+
// value, not the outer literal.
|
|
4515
|
+
expect(template).toContain('{{range $_, $count := .Items}}<li data-key="{{.}}">')
|
|
4516
|
+
})
|
|
4517
|
+
|
|
4518
|
+
// Slice B's guard, by contrast, is the coarse #2212 trade-off (a flat,
|
|
4519
|
+
// scope-blind `Set<string>` with the loop-bound name subtracted
|
|
4520
|
+
// component-wide): a genuinely non-shadowed occurrence OUTSIDE the loop,
|
|
4521
|
+
// whose name happens to be loop-bound elsewhere, also loses its
|
|
4522
|
+
// string-typed classification and falls back to numeric `bf_add`. This is
|
|
4523
|
+
// the accepted, already-documented residual (same as Blade's #2212
|
|
4524
|
+
// comment) — the suppressed case is safe (numeric fallback, never
|
|
4525
|
+
// silently-wrong string output), just imprecise.
|
|
4526
|
+
test('slice B guard is coarse (accepted #2212 trade-off): a string prop shadowed elsewhere loses bf_concat_str even outside the loop', () => {
|
|
4527
|
+
const { template } = compileAndGenerate(`
|
|
4528
|
+
export function Foo({ count, arr }: { count: string; arr: number[] }) {
|
|
4529
|
+
return <div>
|
|
4530
|
+
<p>{1 + count}</p>
|
|
4531
|
+
<ul>{arr.map((count) => <li key={count}>{count}</li>)}</ul>
|
|
4532
|
+
</div>
|
|
4533
|
+
}
|
|
4534
|
+
`)
|
|
4535
|
+
// Coarse trade-off: falls back to numeric bf_add outside the loop too,
|
|
4536
|
+
// even though this occurrence of `count` is the genuinely string-typed
|
|
4537
|
+
// outer prop, not shadowed.
|
|
4538
|
+
expect(template).toContain('bf_add 1 .Count')
|
|
4539
|
+
expect(template).not.toContain('bf_concat_str')
|
|
4540
|
+
})
|
|
4541
|
+
|
|
4542
|
+
test('end-to-end via real `go run`: shadowed const/type resolution renders correct HTML', async () => {
|
|
4543
|
+
try {
|
|
4544
|
+
const html = await renderGoTemplateComponent({
|
|
4545
|
+
source: `
|
|
4546
|
+
'use client'
|
|
4547
|
+
import { createSignal } from '@barefootjs/client'
|
|
4548
|
+
export function List() {
|
|
4549
|
+
const count = 7
|
|
4550
|
+
const [nums] = createSignal<number[]>([2, 5])
|
|
4551
|
+
return <ul>{nums().map((count) => <li key={count}>{count * 3}</li>)}</ul>
|
|
4552
|
+
}
|
|
4553
|
+
`,
|
|
4554
|
+
adapter: new GoTemplateAdapter(),
|
|
4555
|
+
props: {},
|
|
4556
|
+
})
|
|
4557
|
+
// The outer `const count = 7` must never leak into the rendered
|
|
4558
|
+
// per-item output — each `<li>` carries its OWN loop value (2, 5),
|
|
4559
|
+
// not the constant 7.
|
|
4560
|
+
expect(html).toContain('<li data-key="2"><!--bf:s0-->6<!--/--></li>')
|
|
4561
|
+
expect(html).toContain('<li data-key="5"><!--bf:s0-->15<!--/--></li>')
|
|
4562
|
+
expect(html).not.toContain('data-key="7"')
|
|
4563
|
+
} catch (err) {
|
|
4564
|
+
if (err instanceof GoNotAvailableError) {
|
|
4565
|
+
console.log('Skipping #2236 e2e: go command not found')
|
|
4566
|
+
return
|
|
4567
|
+
}
|
|
4568
|
+
throw err
|
|
4569
|
+
}
|
|
4570
|
+
})
|
|
4571
|
+
})
|