@barefootjs/go-template 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/go-template-adapter.d.ts +295 -9
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +1282 -137
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +1283 -136
- package/dist/index.js +1282 -137
- package/dist/test-render.d.ts +8 -0
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/__tests__/build.test.ts +40 -1
- package/src/__tests__/derived-state-memo.test.ts +371 -0
- package/src/__tests__/go-template-adapter.test.ts +267 -7
- package/src/adapter/go-template-adapter.ts +2205 -188
- package/src/build.ts +4 -0
- package/src/test-render.ts +109 -27
package/src/build.ts
CHANGED
|
@@ -165,6 +165,10 @@ export function combineGoTypes(options: {
|
|
|
165
165
|
const usageScan = combinedContent + '\n' + (manualTypes ?? '')
|
|
166
166
|
const stdlibImports: string[] = [`\t"math/rand"`]
|
|
167
167
|
if (/\bfmt\./.test(usageScan)) stdlibImports.push(`\t"fmt"`)
|
|
168
|
+
// `strings.` shows up in generated constructors that normalize a prop, e.g.
|
|
169
|
+
// `searchParams()`-backed components emit `strings.TrimRight(in.Base, "/")`
|
|
170
|
+
// to derive the router base. Only imported when referenced, like the others.
|
|
171
|
+
if (/\bstrings\./.test(usageScan)) stdlibImports.push(`\t"strings"`)
|
|
168
172
|
if (/\btemplate\.HTML\b/.test(usageScan)) stdlibImports.push(`\t"html/template"`)
|
|
169
173
|
// gofmt sorts the stdlib group alphabetically by import path.
|
|
170
174
|
stdlibImports.sort()
|
package/src/test-render.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { compileJSX } from '@barefootjs/jsx'
|
|
9
9
|
import type { TemplateAdapter, ComponentIR } from '@barefootjs/jsx'
|
|
10
10
|
import { GoTemplateAdapter } from './adapter/go-template-adapter.ts'
|
|
11
|
+
import { deduplicateGoTypes } from './build.ts'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Capitalize a JSON key to its Go struct field name using the same
|
|
@@ -73,6 +74,14 @@ export interface RenderOptions {
|
|
|
73
74
|
props?: Record<string, unknown>
|
|
74
75
|
/** Additional component files (filename → source) */
|
|
75
76
|
components?: Record<string, string>
|
|
77
|
+
/**
|
|
78
|
+
* Pre-compiled child SSR modules (import specifier → path) — consumed
|
|
79
|
+
* by the Hono renderer; here only the KEYS matter (#1896): they carry
|
|
80
|
+
* EVERY specifier a sibling is reachable by (`@ui/components/ui/icon`
|
|
81
|
+
* and `../icon`), whereas `components` keys carry one per sibling, so
|
|
82
|
+
* the sibling-import recognition uses both.
|
|
83
|
+
*/
|
|
84
|
+
componentModules?: Record<string, string>
|
|
76
85
|
/**
|
|
77
86
|
* Explicit component to render when `source` declares multiple
|
|
78
87
|
* exports (e.g. `ReactiveProps.tsx` → `PropsReactivityComparison`).
|
|
@@ -83,7 +92,7 @@ export interface RenderOptions {
|
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
export async function renderGoTemplateComponent(options: RenderOptions): Promise<string> {
|
|
86
|
-
const { source, adapter, props, components, componentName: requestedName } = options
|
|
95
|
+
const { source, adapter, props, components, componentModules, componentName: requestedName } = options
|
|
87
96
|
|
|
88
97
|
if (!adapter.generateTypes) {
|
|
89
98
|
throw new Error('Go Template adapter must implement generateTypes()')
|
|
@@ -105,7 +114,22 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
|
|
|
105
114
|
importedNames: string[]
|
|
106
115
|
}
|
|
107
116
|
const childArtifacts = new Map<string, ChildComponentArtifacts>()
|
|
117
|
+
// Import specifiers the harness holds child sources for — recognised as
|
|
118
|
+
// sibling imports alongside relative ones (see
|
|
119
|
+
// `collectImportedComponentNames`).
|
|
120
|
+
const childSpecifiers = new Set([
|
|
121
|
+
...Object.keys(components ?? {}),
|
|
122
|
+
...Object.keys(componentModules ?? {}),
|
|
123
|
+
])
|
|
108
124
|
if (components) {
|
|
125
|
+
// Two passes (#1896): a child file may itself render a component from a
|
|
126
|
+
// *sibling* child file (data-table's `checkbox` renders `<CheckIcon
|
|
127
|
+
// data-slot=.../>` from `icon`). `generateTypes` routes such non-param
|
|
128
|
+
// attributes through the target's registered shape, so EVERY child's
|
|
129
|
+
// shape must be registered before ANY child's types are generated —
|
|
130
|
+
// otherwise processing order decides whether the attribute lands in
|
|
131
|
+
// the rest bag or becomes an invalid hyphenated Go field.
|
|
132
|
+
const compiledChildIrs: Array<{ ir: ComponentIR; defines: Map<string, string> }> = []
|
|
109
133
|
for (const [filename, childSource] of Object.entries(components)) {
|
|
110
134
|
const childResult = compileJSX(childSource, filename, { adapter, outputIR: true })
|
|
111
135
|
const childErrors = childResult.errors.filter(e => e.severity === 'error')
|
|
@@ -119,26 +143,29 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
|
|
|
119
143
|
const childIrFiles = childResult.files.filter(f => f.type === 'ir')
|
|
120
144
|
for (const childIrFile of childIrFiles) {
|
|
121
145
|
const childIR = JSON.parse(childIrFile.content) as ComponentIR
|
|
122
|
-
const name = childIR.metadata.componentName
|
|
123
146
|
// (#checkbox) Register each child's cross-component shape so the
|
|
124
147
|
// parent's static-child-init codegen can route a non-param attribute
|
|
125
148
|
// (`<CheckIcon data-slot=.../>`) into the child's rest bag instead of
|
|
126
149
|
// an invalid hyphenated Go field. No-op on adapters without the hook.
|
|
127
150
|
registerChildShape(adapter, childIR)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
})
|
|
151
|
+
compiledChildIrs.push({ ir: childIR, defines: defineBlocks })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const { ir: childIR, defines } of compiledChildIrs) {
|
|
155
|
+
const name = childIR.metadata.componentName
|
|
156
|
+
let typeBlock: string | null = adapter.generateTypes!(childIR)
|
|
157
|
+
if (typeBlock) {
|
|
158
|
+
// Strip package declaration and imports — will be merged into main types
|
|
159
|
+
typeBlock = typeBlock.replace(/^package \w+\n*/, '')
|
|
160
|
+
typeBlock = typeBlock.replace(/import\s*\([^)]*\)\n*/g, '')
|
|
161
|
+
typeBlock = typeBlock.replace(/\t"math\/rand"\n/g, '')
|
|
162
|
+
typeBlock = typeBlock.trim()
|
|
141
163
|
}
|
|
164
|
+
childArtifacts.set(name, {
|
|
165
|
+
define: defines.get(name) ?? '',
|
|
166
|
+
typeBlock,
|
|
167
|
+
importedNames: collectImportedComponentNames(childIR, childSpecifiers),
|
|
168
|
+
})
|
|
142
169
|
}
|
|
143
170
|
}
|
|
144
171
|
|
|
@@ -183,7 +210,13 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
|
|
|
183
210
|
// child-collection loop above).
|
|
184
211
|
const reachableChildNames = new Set<string>()
|
|
185
212
|
{
|
|
186
|
-
|
|
213
|
+
// Seed from EVERY component of the parent source, not just the entry:
|
|
214
|
+
// the siblings' type blocks are merged into the unit too, and their
|
|
215
|
+
// constructors reference their own child components' Props types
|
|
216
|
+
// (#1896 — dropdown-menu-demo's profile sibling uses SettingsIcon,
|
|
217
|
+
// which the entry export never imports... the import list is per-file,
|
|
218
|
+
// but per-IR metadata can differ after analysis; union them all).
|
|
219
|
+
const queue = irs.flatMap(i => collectImportedComponentNames(i, childSpecifiers))
|
|
187
220
|
while (queue.length > 0) {
|
|
188
221
|
const name = queue.shift()!
|
|
189
222
|
if (reachableChildNames.has(name)) continue
|
|
@@ -215,7 +248,8 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
|
|
|
215
248
|
// Remove "math/rand" import from types (randomID is defined in main.go)
|
|
216
249
|
goTypes = goTypes.replace(/\t"math\/rand"\n/, '')
|
|
217
250
|
|
|
218
|
-
//
|
|
251
|
+
// Collect sibling-component type definitions (multi-component source).
|
|
252
|
+
const siblingTypeBlocks: string[] = []
|
|
219
253
|
for (const siblingIR of irs) {
|
|
220
254
|
if (siblingIR === ir) continue
|
|
221
255
|
let siblingTypes = adapter.generateTypes(siblingIR)
|
|
@@ -223,12 +257,27 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
|
|
|
223
257
|
siblingTypes = siblingTypes.replace(/^package \w+\n*/, '')
|
|
224
258
|
siblingTypes = siblingTypes.replace(/import\s*\([^)]*\)\n*/g, '')
|
|
225
259
|
siblingTypes = siblingTypes.replace(/\t"math\/rand"\n/g, '')
|
|
226
|
-
|
|
260
|
+
siblingTypeBlocks.push(siblingTypes.trim())
|
|
227
261
|
}
|
|
228
262
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
263
|
+
// Merge entry + sibling + child type blocks through the same
|
|
264
|
+
// `deduplicateGoTypes` helper `bf build` uses. Duplicates arise in two
|
|
265
|
+
// ways (#1896): a multi-component file emits its module-scope shared
|
|
266
|
+
// types (a context-value struct, a data `type Payment = …`) once per
|
|
267
|
+
// component IR — both across the entry source's own sibling exports
|
|
268
|
+
// (data-table-demo's `Payment redeclared`) and across a child file's
|
|
269
|
+
// components (radio-group's context value). The dedup helper re-inserts
|
|
270
|
+
// type definitions at the top of its input, so the entry's
|
|
271
|
+
// `package main` + import header is split off first and re-prepended.
|
|
272
|
+
if (siblingTypeBlocks.length > 0 || childTypeBlocks.length > 0) {
|
|
273
|
+
const headerMatch = goTypes.match(/^package main\n+import \([\s\S]*?\)\n+/)
|
|
274
|
+
const header = headerMatch ? headerMatch[0] : ''
|
|
275
|
+
const body = goTypes.slice(header.length)
|
|
276
|
+
goTypes =
|
|
277
|
+
header +
|
|
278
|
+
deduplicateGoTypes(
|
|
279
|
+
[body, ...siblingTypeBlocks, ...childTypeBlocks].join('\n\n'),
|
|
280
|
+
)
|
|
232
281
|
}
|
|
233
282
|
|
|
234
283
|
// Sibling / child type blocks have their own `import (...)` stripped above
|
|
@@ -322,7 +371,11 @@ func randomID(n int) string {
|
|
|
322
371
|
}
|
|
323
372
|
|
|
324
373
|
func main() {
|
|
325
|
-
|
|
374
|
+
// Two-step Funcs: bf_tmpl (children defines, #1896) needs a closure
|
|
375
|
+
// over the same template set the defines are parsed into.
|
|
376
|
+
root := template.New("").Funcs(bfTestFuncMap())
|
|
377
|
+
root = root.Funcs(bf.TemplateFuncMap(root))
|
|
378
|
+
tmpl := template.Must(root.Parse(tmplContent))
|
|
326
379
|
props := New${componentName}Props(${componentName}Input{
|
|
327
380
|
ScopeID: ${JSON.stringify(rootScopeId)},
|
|
328
381
|
${propsInit}
|
|
@@ -360,7 +413,23 @@ ${propsInit}
|
|
|
360
413
|
|
|
361
414
|
const exitCode = await proc.exited
|
|
362
415
|
if (exitCode !== 0) {
|
|
363
|
-
|
|
416
|
+
// Append the offending `types.go` lines (with two lines of context)
|
|
417
|
+
// for each `./types.go:NN` the compiler reported — the temp dir is
|
|
418
|
+
// deleted in `finally`, so without this the merged unit that
|
|
419
|
+
// actually failed is unrecoverable (#1896 debugging aid).
|
|
420
|
+
const typeLines = goTypes.split('\n')
|
|
421
|
+
const context: string[] = []
|
|
422
|
+
const seen = new Set<number>()
|
|
423
|
+
for (const m of stderr.matchAll(/\.\/types\.go:(\d+)/g)) {
|
|
424
|
+
const n = Number(m[1])
|
|
425
|
+
for (let i = Math.max(1, n - 2); i <= Math.min(typeLines.length, n + 2); i++) {
|
|
426
|
+
if (seen.has(i)) continue
|
|
427
|
+
seen.add(i)
|
|
428
|
+
context.push(`${i === n ? '>' : ' '} types.go:${i}: ${typeLines[i - 1]}`)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const detail = context.length > 0 ? `\n${context.join('\n')}` : ''
|
|
432
|
+
throw new Error(`go run failed (exit ${exitCode}):\n${stderr}${detail}`)
|
|
364
433
|
}
|
|
365
434
|
|
|
366
435
|
return stdout
|
|
@@ -418,14 +487,23 @@ function splitTemplateDefines(content: string): Map<string, string> {
|
|
|
418
487
|
|
|
419
488
|
/**
|
|
420
489
|
* Component names a component IR imports from sibling source files — i.e.
|
|
421
|
-
* non-type imports from relative (`./` / `../`) specifiers
|
|
422
|
-
* the
|
|
490
|
+
* non-type imports from relative (`./` / `../`) specifiers, plus any
|
|
491
|
+
* specifier the harness was explicitly handed a child source for
|
|
492
|
+
* (`childSpecifiers`, the `components` map keys). The latter is how
|
|
493
|
+
* demo-corpus roots reach their children: they import primitives through
|
|
494
|
+
* the `@ui/components/ui/<name>` alias (#1467), which the relative-only
|
|
495
|
+
* test would silently drop — emitting a parent that references
|
|
496
|
+
* `New<Child>Props` without the child's type block. Used to compute the
|
|
497
|
+
* transitive set of child components the combined Go unit needs.
|
|
423
498
|
*/
|
|
424
|
-
function collectImportedComponentNames(
|
|
499
|
+
function collectImportedComponentNames(
|
|
500
|
+
ir: ComponentIR,
|
|
501
|
+
childSpecifiers?: ReadonlySet<string>,
|
|
502
|
+
): string[] {
|
|
425
503
|
const names: string[] = []
|
|
426
504
|
for (const imp of ir.metadata.imports ?? []) {
|
|
427
505
|
if (imp.isTypeOnly) continue
|
|
428
|
-
if (!imp.source.startsWith('.')) continue
|
|
506
|
+
if (!imp.source.startsWith('.') && !childSpecifiers?.has(imp.source)) continue
|
|
429
507
|
for (const spec of imp.specifiers ?? []) {
|
|
430
508
|
if (spec.isNamespace) continue
|
|
431
509
|
// Imported binding name (the local alias is what JSX references, but the
|
|
@@ -457,6 +535,10 @@ function registerChildShape(adapter: TemplateAdapter, ir: ComponentIR): void {
|
|
|
457
535
|
*/
|
|
458
536
|
const MERGED_STDLIB_IMPORTS: Array<{ path: string; usage: RegExp }> = [
|
|
459
537
|
{ path: 'fmt', usage: /\bfmt\./ },
|
|
538
|
+
// A child whose constructor bakes static JSX children references
|
|
539
|
+
// `template.HTML(...)` (#1896 — the `command` demo's Kbd child); the
|
|
540
|
+
// entry component may not use html/template itself.
|
|
541
|
+
{ path: 'html/template', usage: /\btemplate\.HTML\(/ },
|
|
460
542
|
]
|
|
461
543
|
|
|
462
544
|
/**
|