@barefootjs/go-template 0.8.0 → 0.9.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 +142 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +402 -27
- package/dist/build.js +402 -27
- package/dist/index.js +402 -27
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/go-template-adapter.test.ts +189 -29
- package/src/adapter/go-template-adapter.ts +689 -25
- package/src/test-render.ts +189 -10
package/src/test-render.ts
CHANGED
|
@@ -68,9 +68,22 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
|
|
|
68
68
|
throw new Error('Go Template adapter must implement generateTypes()')
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// Compile child components first
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
// Compile child components first, keeping per-component template defines /
|
|
72
|
+
// type blocks keyed by component name. A child *file* can export many
|
|
73
|
+
// components (e.g. `../icon` exports 30+ icons); only the ones the parent
|
|
74
|
+
// transitively references are emitted into the combined unit. Emitting all
|
|
75
|
+
// of them concatenates dead components whose own codegen may not compile
|
|
76
|
+
// (e.g. `ChevronDownIcon`'s `strokePaths['chevron-down']` lowers to an
|
|
77
|
+
// invalid `{{.StrokePaths.Chevron-down}}` field reference), which would
|
|
78
|
+
// break the whole template parse even though the parent never uses them.
|
|
79
|
+
// (#checkbox)
|
|
80
|
+
interface ChildComponentArtifacts {
|
|
81
|
+
define: string
|
|
82
|
+
typeBlock: string | null
|
|
83
|
+
/** Component names this child component imports from sibling files. */
|
|
84
|
+
importedNames: string[]
|
|
85
|
+
}
|
|
86
|
+
const childArtifacts = new Map<string, ChildComponentArtifacts>()
|
|
74
87
|
if (components) {
|
|
75
88
|
for (const [filename, childSource] of Object.entries(components)) {
|
|
76
89
|
const childResult = compileJSX(childSource, filename, { adapter, outputIR: true })
|
|
@@ -80,19 +93,30 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
|
|
|
80
93
|
}
|
|
81
94
|
const childTemplate = childResult.files.find(f => f.type === 'markedTemplate')
|
|
82
95
|
if (!childTemplate) throw new Error(`No marked template for ${filename}`)
|
|
83
|
-
|
|
96
|
+
const defineBlocks = splitTemplateDefines(childTemplate.content)
|
|
84
97
|
|
|
85
98
|
const childIrFiles = childResult.files.filter(f => f.type === 'ir')
|
|
86
99
|
for (const childIrFile of childIrFiles) {
|
|
87
100
|
const childIR = JSON.parse(childIrFile.content) as ComponentIR
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
const name = childIR.metadata.componentName
|
|
102
|
+
// (#checkbox) Register each child's cross-component shape so the
|
|
103
|
+
// parent's static-child-init codegen can route a non-param attribute
|
|
104
|
+
// (`<CheckIcon data-slot=.../>`) into the child's rest bag instead of
|
|
105
|
+
// an invalid hyphenated Go field. No-op on adapters without the hook.
|
|
106
|
+
registerChildShape(adapter, childIR)
|
|
107
|
+
let typeBlock: string | null = adapter.generateTypes!(childIR)
|
|
108
|
+
if (typeBlock) {
|
|
90
109
|
// Strip package declaration and imports — will be merged into main types
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
110
|
+
typeBlock = typeBlock.replace(/^package \w+\n*/, '')
|
|
111
|
+
typeBlock = typeBlock.replace(/import\s*\([^)]*\)\n*/g, '')
|
|
112
|
+
typeBlock = typeBlock.replace(/\t"math\/rand"\n/g, '')
|
|
113
|
+
typeBlock = typeBlock.trim()
|
|
95
114
|
}
|
|
115
|
+
childArtifacts.set(name, {
|
|
116
|
+
define: defineBlocks.get(name) ?? '',
|
|
117
|
+
typeBlock,
|
|
118
|
+
importedNames: collectImportedComponentNames(childIR),
|
|
119
|
+
})
|
|
96
120
|
}
|
|
97
121
|
}
|
|
98
122
|
}
|
|
@@ -120,6 +144,37 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
|
|
|
120
144
|
irs.find(i => i.metadata.isExported) ??
|
|
121
145
|
irs[0]
|
|
122
146
|
|
|
147
|
+
// (#checkbox) Register each in-source sibling's shape too, so a parent
|
|
148
|
+
// rendering an inline sibling routes non-param attributes into the
|
|
149
|
+
// sibling's rest bag (same fix as the auto-inferred `../<name>` children).
|
|
150
|
+
for (const siblingIR of irs) registerChildShape(adapter, siblingIR)
|
|
151
|
+
|
|
152
|
+
// (#checkbox) Resolve which child components the combined unit actually
|
|
153
|
+
// needs: start from the entry component's cross-file imports, then close
|
|
154
|
+
// transitively over each included child's own imports. Components a child
|
|
155
|
+
// *file* exports but nobody references are dropped (see comment at the
|
|
156
|
+
// child-collection loop above).
|
|
157
|
+
const reachableChildNames = new Set<string>()
|
|
158
|
+
{
|
|
159
|
+
const queue = [...collectImportedComponentNames(ir)]
|
|
160
|
+
while (queue.length > 0) {
|
|
161
|
+
const name = queue.shift()!
|
|
162
|
+
if (reachableChildNames.has(name)) continue
|
|
163
|
+
const artifact = childArtifacts.get(name)
|
|
164
|
+
if (!artifact) continue // not a compiled child (e.g. an in-source sibling)
|
|
165
|
+
reachableChildNames.add(name)
|
|
166
|
+
for (const dep of artifact.importedNames) queue.push(dep)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const childTemplates: string[] = []
|
|
170
|
+
const childTypeBlocks: string[] = []
|
|
171
|
+
for (const name of reachableChildNames) {
|
|
172
|
+
const artifact = childArtifacts.get(name)
|
|
173
|
+
if (!artifact) continue
|
|
174
|
+
if (artifact.define) childTemplates.push(artifact.define)
|
|
175
|
+
if (artifact.typeBlock) childTypeBlocks.push(artifact.typeBlock)
|
|
176
|
+
}
|
|
177
|
+
|
|
123
178
|
// Generate types for the entry-point component first, then append
|
|
124
179
|
// types for every sibling component in the same source file so the
|
|
125
180
|
// generated `types.go` is self-contained (multi-component test
|
|
@@ -149,6 +204,18 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
|
|
|
149
204
|
goTypes += '\n\n' + childTypeBlocks.join('\n\n')
|
|
150
205
|
}
|
|
151
206
|
|
|
207
|
+
// Sibling / child type blocks have their own `import (...)` stripped above
|
|
208
|
+
// (their package decl + imports are merged into the entry component's
|
|
209
|
+
// single `types.go` block). When a sibling's generated constructor uses a
|
|
210
|
+
// standard-library symbol the entry component doesn't — e.g. CheckIcon's
|
|
211
|
+
// `NewCheckIconProps` calls `fmt.Sprint(...)` for a `Record[key]` spread
|
|
212
|
+
// lookup (#checkbox icon) — that import would be lost, producing
|
|
213
|
+
// `undefined: fmt`. Re-add any such import to the entry component's import
|
|
214
|
+
// block so the combined compilation unit resolves the symbol. Only `fmt`
|
|
215
|
+
// is needed today; extend `MERGED_STDLIB_IMPORTS` if a future sibling pulls
|
|
216
|
+
// in another stdlib package.
|
|
217
|
+
goTypes = ensureMergedStdlibImports(goTypes)
|
|
218
|
+
|
|
152
219
|
const componentName = ir.metadata.componentName
|
|
153
220
|
// Concatenate all templates (child define blocks + parent)
|
|
154
221
|
const template = [...childTemplates, templateFile.content].join('\n')
|
|
@@ -274,6 +341,118 @@ ${propsInit}
|
|
|
274
341
|
}
|
|
275
342
|
}
|
|
276
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Split a marked-template file's content into per-component `{{define "X"}}…
|
|
346
|
+
* {{end}}` blocks, keyed by component name. A child file that exports multiple
|
|
347
|
+
* components yields one entry per component so the harness can emit only the
|
|
348
|
+
* referenced ones (#checkbox). Non-define preamble (e.g. `export type {...}`)
|
|
349
|
+
* is ignored — it isn't valid Go template text and the entry component carries
|
|
350
|
+
* its own.
|
|
351
|
+
*/
|
|
352
|
+
function splitTemplateDefines(content: string): Map<string, string> {
|
|
353
|
+
const out = new Map<string, string>()
|
|
354
|
+
// Block actions that open a scope closed by `{{end}}`. A `{{define}}` body
|
|
355
|
+
// can contain nested `{{if}}` / `{{range}}` / `{{with}}` / `{{block}}`, so a
|
|
356
|
+
// non-greedy `.*?{{end}}` would stop at the first inner `{{end}}`. Track
|
|
357
|
+
// nesting depth and capture from each `{{define}}` to its matching `{{end}}`.
|
|
358
|
+
const actionRe = /\{\{[-\s]*(define|if|range|with|block|end)\b/g
|
|
359
|
+
let m: RegExpExecArray | null
|
|
360
|
+
let openName: string | null = null
|
|
361
|
+
let openStart = 0
|
|
362
|
+
let depth = 0
|
|
363
|
+
while ((m = actionRe.exec(content)) !== null) {
|
|
364
|
+
const kw = m[1]
|
|
365
|
+
if (kw === 'end') {
|
|
366
|
+
if (openName !== null) {
|
|
367
|
+
depth--
|
|
368
|
+
if (depth === 0) {
|
|
369
|
+
// Extend to the close of this `{{...end...}}` action.
|
|
370
|
+
const closeIdx = content.indexOf('}}', m.index)
|
|
371
|
+
const end = closeIdx === -1 ? content.length : closeIdx + 2
|
|
372
|
+
out.set(openName, content.slice(openStart, end))
|
|
373
|
+
openName = null
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
continue
|
|
377
|
+
}
|
|
378
|
+
if (kw === 'define' && openName === null) {
|
|
379
|
+
const nameMatch = content.slice(m.index).match(/^\{\{[-\s]*define\s+"([^"]+)"/)
|
|
380
|
+
openName = nameMatch ? nameMatch[1] : null
|
|
381
|
+
openStart = m.index
|
|
382
|
+
depth = 1
|
|
383
|
+
continue
|
|
384
|
+
}
|
|
385
|
+
// Any opening action while inside a define deepens the nesting.
|
|
386
|
+
if (openName !== null) depth++
|
|
387
|
+
}
|
|
388
|
+
return out
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Component names a component IR imports from sibling source files — i.e.
|
|
393
|
+
* non-type imports from relative (`./` / `../`) specifiers. Used to compute
|
|
394
|
+
* the transitive set of child components the combined Go unit needs.
|
|
395
|
+
*/
|
|
396
|
+
function collectImportedComponentNames(ir: ComponentIR): string[] {
|
|
397
|
+
const names: string[] = []
|
|
398
|
+
for (const imp of ir.metadata.imports ?? []) {
|
|
399
|
+
if (imp.isTypeOnly) continue
|
|
400
|
+
if (!imp.source.startsWith('.')) continue
|
|
401
|
+
for (const spec of imp.specifiers ?? []) {
|
|
402
|
+
if (spec.isNamespace) continue
|
|
403
|
+
// Imported binding name (the local alias is what JSX references, but the
|
|
404
|
+
// generated `{{define}}`/types are keyed by the exported component name —
|
|
405
|
+
// which equals the specifier name when un-aliased; aliased component
|
|
406
|
+
// imports aren't exercised by the UI fixtures).
|
|
407
|
+
names.push(spec.alias ?? spec.name)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return names
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Register a child/sibling component's cross-component shape on the adapter
|
|
415
|
+
* when it supports the `registerChildComponentShape` hook (Go template). A
|
|
416
|
+
* no-op on adapters without it. Lets the parent's static-child-init codegen
|
|
417
|
+
* route a non-param attribute into the child's rest bag (#checkbox).
|
|
418
|
+
*/
|
|
419
|
+
function registerChildShape(adapter: TemplateAdapter, ir: ComponentIR): void {
|
|
420
|
+
const hook = (adapter as { registerChildComponentShape?: (ir: ComponentIR) => void })
|
|
421
|
+
.registerChildComponentShape
|
|
422
|
+
if (typeof hook === 'function') hook.call(adapter, ir)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Standard-library packages that a merged sibling/child type block may
|
|
427
|
+
* reference but the entry component's own import block omits. Each entry maps
|
|
428
|
+
* the import path to a `<pkg>.` usage probe.
|
|
429
|
+
*/
|
|
430
|
+
const MERGED_STDLIB_IMPORTS: Array<{ path: string; usage: RegExp }> = [
|
|
431
|
+
{ path: 'fmt', usage: /\bfmt\./ },
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Ensure any standard-library import a merged sibling/child block needs is
|
|
436
|
+
* present in the entry component's `import (...)` block. Sibling/child blocks
|
|
437
|
+
* have their own imports stripped during merge, so a symbol they reference
|
|
438
|
+
* (e.g. `fmt.Sprint`) is otherwise `undefined` in the combined unit.
|
|
439
|
+
*/
|
|
440
|
+
function ensureMergedStdlibImports(goTypes: string): string {
|
|
441
|
+
const importMatch = goTypes.match(/import\s*\(([^)]*)\)/)
|
|
442
|
+
if (!importMatch) return goTypes
|
|
443
|
+
const block = importMatch[1]
|
|
444
|
+
const additions: string[] = []
|
|
445
|
+
for (const { path, usage } of MERGED_STDLIB_IMPORTS) {
|
|
446
|
+
if (!usage.test(goTypes)) continue
|
|
447
|
+
// Already imported? (quoted path present in the import block)
|
|
448
|
+
if (block.includes(`"${path}"`)) continue
|
|
449
|
+
additions.push(`\t"${path}"`)
|
|
450
|
+
}
|
|
451
|
+
if (additions.length === 0) return goTypes
|
|
452
|
+
const newBlock = `import (\n${additions.join('\n')}\n${block.replace(/^\n+/, '')})`
|
|
453
|
+
return goTypes.replace(/import\s*\([^)]*\)/, newBlock)
|
|
454
|
+
}
|
|
455
|
+
|
|
277
456
|
/**
|
|
278
457
|
* Build Go struct field initializers from props.
|
|
279
458
|
*/
|