@barefootjs/go-template 0.30.6 → 0.31.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/go-template",
3
- "version": "0.30.6",
3
+ "version": "0.31.0",
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",
@@ -17,9 +17,9 @@
17
17
  "./test-render": {
18
18
  "bun": "./src/test-render.ts"
19
19
  },
20
- "./build": {
21
- "types": "./dist/build.d.ts",
22
- "import": "./dist/build.js"
20
+ "./vite": {
21
+ "types": "./dist/vite.d.ts",
22
+ "import": "./dist/vite.js"
23
23
  }
24
24
  },
25
25
  "files": [
@@ -28,7 +28,7 @@
28
28
  ],
29
29
  "scripts": {
30
30
  "build": "bun run build:js && bun run build:types",
31
- "build:js": "bun build ./src/index.ts ./src/adapter/index.ts ./src/build.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared --external typescript",
31
+ "build:js": "bun build ./src/index.ts ./src/adapter/index.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared --external @barefootjs/vite --external vite --external typescript && bun build ./src/vite.ts --outfile ./dist/vite.js --format esm --target node --external @barefootjs/vite --external vite --external typescript",
32
32
  "build:types": "tsgo --emitDeclarationOnly --outDir ./dist",
33
33
  "test": "bun test",
34
34
  "clean": "rm -rf dist",
@@ -49,14 +49,27 @@
49
49
  "directory": "packages/adapter-go-template"
50
50
  },
51
51
  "dependencies": {
52
- "@barefootjs/shared": "0.30.6"
52
+ "@barefootjs/shared": "0.31.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@barefootjs/jsx": ">=0.2.0",
56
- "typescript": "^5.0.0"
56
+ "@barefootjs/vite": ">=0.2.0",
57
+ "typescript": "^5.0.0",
58
+ "vite": "^6.0.0"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "@barefootjs/vite": {
62
+ "optional": true
63
+ },
64
+ "vite": {
65
+ "optional": true
66
+ }
57
67
  },
58
68
  "devDependencies": {
59
69
  "@barefootjs/adapter-tests": "0.1.0",
60
- "@barefootjs/jsx": "0.30.6"
70
+ "@barefootjs/client": "0.31.0",
71
+ "@barefootjs/jsx": "0.31.0",
72
+ "@barefootjs/vite": "0.31.0",
73
+ "vite": "^6.0.0"
61
74
  }
62
75
  }
@@ -5619,3 +5619,148 @@ export function C({ items, enabled }: { items: Item[]; enabled: boolean }) {
5619
5619
  }
5620
5620
  })
5621
5621
  })
5622
+
5623
+ describe('GoTemplateAdapter - scriptAssets (Vite late-binding)', () => {
5624
+ const CLIENT_COMPONENT = `
5625
+ 'use client'
5626
+ import { createSignal } from '@barefootjs/client'
5627
+ export function Counter() {
5628
+ const [count, setCount] = createSignal(0)
5629
+ return <button onClick={() => setCount(count() + 1)}>{count()}</button>
5630
+ }
5631
+ `
5632
+
5633
+ test('emits one {{.Scripts.Register}} per URL, in order, when scriptAssets is set', () => {
5634
+ const ir = compileToIR(CLIENT_COMPONENT)
5635
+ const { template } = new GoTemplateAdapter().generate(ir, {
5636
+ scriptAssets: ['/assets/runtime-abc123.js', '/assets/counter-def456.js'],
5637
+ })
5638
+ const runtimeIdx = template.indexOf('{{.Scripts.Register "/assets/runtime-abc123.js"}}')
5639
+ const compIdx = template.indexOf('{{.Scripts.Register "/assets/counter-def456.js"}}')
5640
+ expect(runtimeIdx).toBeGreaterThanOrEqual(0)
5641
+ expect(compIdx).toBeGreaterThanOrEqual(0)
5642
+ expect(runtimeIdx).toBeLessThan(compIdx)
5643
+ expect(template).toContain('{{if .Scripts}}')
5644
+ expect(template).not.toContain('/static/client/barefoot.js')
5645
+ expect(template).not.toContain('Counter.client.js')
5646
+ })
5647
+
5648
+ test('emits a single registration for a single-element scriptAssets array', () => {
5649
+ const ir = compileToIR(CLIENT_COMPONENT)
5650
+ const { template } = new GoTemplateAdapter().generate(ir, {
5651
+ scriptAssets: ['/assets/only-one.js'],
5652
+ })
5653
+ expect(template).toContain('{{.Scripts.Register "/assets/only-one.js"}}')
5654
+ expect(template.match(/\.Scripts\.Register/g)?.length).toBe(1)
5655
+ })
5656
+
5657
+ test('an empty scriptAssets array emits no script registrations', () => {
5658
+ const ir = compileToIR(CLIENT_COMPONENT)
5659
+ const { template } = new GoTemplateAdapter().generate(ir, { scriptAssets: [] })
5660
+ expect(template).not.toContain('.Scripts.Register')
5661
+ expect(template).not.toContain('{{if .Scripts}}')
5662
+ })
5663
+
5664
+ test('skipScriptRegistration still wins when scriptAssets is also set', () => {
5665
+ const ir = compileToIR(CLIENT_COMPONENT)
5666
+ const { template } = new GoTemplateAdapter().generate(ir, {
5667
+ skipScriptRegistration: true,
5668
+ scriptAssets: ['/assets/should-not-appear.js'],
5669
+ })
5670
+ expect(template).not.toContain('.Scripts.Register')
5671
+ })
5672
+
5673
+ test('absent scriptAssets falls back to adapter-computed script paths', () => {
5674
+ const ir = compileToIR(CLIENT_COMPONENT)
5675
+ const computed = new GoTemplateAdapter().generate(ir).template
5676
+ const explicitUndefined = new GoTemplateAdapter().generate(ir, { scriptAssets: undefined }).template
5677
+ expect(computed).toContain('{{.Scripts.Register "/static/client/barefoot.js"}}')
5678
+ expect(computed).toContain('{{.Scripts.Register "/static/client/Counter.client.js"}}')
5679
+ expect(explicitUndefined).toBe(computed)
5680
+ })
5681
+ })
5682
+
5683
+ describe('GoTemplateAdapter - preloadAssets', () => {
5684
+ const CLIENT_COMPONENT = `
5685
+ 'use client'
5686
+ import { createSignal } from '@barefootjs/client'
5687
+ export function Counter() {
5688
+ const [count, setCount] = createSignal(0)
5689
+ return <button onClick={() => setCount(count() + 1)}>{count()}</button>
5690
+ }
5691
+ `
5692
+
5693
+ test('non-empty preloadAssets + non-empty scriptAssets: preload registrations emitted, in order, before script registrations', () => {
5694
+ const ir = compileToIR(CLIENT_COMPONENT)
5695
+ const { template } = new GoTemplateAdapter().generate(ir, {
5696
+ scriptAssets: ['/assets/runtime-abc123.js', '/assets/counter-def456.js'],
5697
+ preloadAssets: ['/assets/index-pre1.js', '/assets/shared-pre2.js'],
5698
+ })
5699
+ const pre1Idx = template.indexOf('{{.Scripts.RegisterPreload "/assets/index-pre1.js"}}')
5700
+ const pre2Idx = template.indexOf('{{.Scripts.RegisterPreload "/assets/shared-pre2.js"}}')
5701
+ const script1Idx = template.indexOf('{{.Scripts.Register "/assets/runtime-abc123.js"}}')
5702
+ const script2Idx = template.indexOf('{{.Scripts.Register "/assets/counter-def456.js"}}')
5703
+ expect(pre1Idx).toBeGreaterThanOrEqual(0)
5704
+ expect(pre2Idx).toBeGreaterThan(pre1Idx)
5705
+ expect(script1Idx).toBeGreaterThan(pre2Idx)
5706
+ expect(script2Idx).toBeGreaterThan(script1Idx)
5707
+ })
5708
+
5709
+ test('preloadAssets: [] emits no preload registration', () => {
5710
+ const ir = compileToIR(CLIENT_COMPONENT)
5711
+ const { template } = new GoTemplateAdapter().generate(ir, {
5712
+ scriptAssets: ['/assets/runtime-abc123.js'],
5713
+ preloadAssets: [],
5714
+ })
5715
+ expect(template).not.toContain('RegisterPreload')
5716
+ expect(template).toContain('{{.Scripts.Register "/assets/runtime-abc123.js"}}')
5717
+ })
5718
+
5719
+ test('preloadAssets: undefined emits no preload registration', () => {
5720
+ const ir = compileToIR(CLIENT_COMPONENT)
5721
+ const { template } = new GoTemplateAdapter().generate(ir, {
5722
+ scriptAssets: ['/assets/runtime-abc123.js'],
5723
+ preloadAssets: undefined,
5724
+ })
5725
+ expect(template).not.toContain('RegisterPreload')
5726
+ expect(template).toContain('{{.Scripts.Register "/assets/runtime-abc123.js"}}')
5727
+ })
5728
+
5729
+ test('preloadAssets non-empty but scriptAssets: [] emits no preload registration (preloads are only meaningful alongside a real script)', () => {
5730
+ const ir = compileToIR(CLIENT_COMPONENT)
5731
+ const { template } = new GoTemplateAdapter().generate(ir, {
5732
+ scriptAssets: [],
5733
+ preloadAssets: ['/assets/index-pre1.js'],
5734
+ })
5735
+ expect(template).not.toContain('RegisterPreload')
5736
+ expect(template).not.toContain('.Scripts.Register')
5737
+ })
5738
+
5739
+ test('skipScriptRegistration: true suppresses both preloads and scripts', () => {
5740
+ const ir = compileToIR(CLIENT_COMPONENT)
5741
+ const { template } = new GoTemplateAdapter().generate(ir, {
5742
+ skipScriptRegistration: true,
5743
+ scriptAssets: ['/assets/runtime-abc123.js'],
5744
+ preloadAssets: ['/assets/index-pre1.js'],
5745
+ })
5746
+ expect(template).not.toContain('RegisterPreload')
5747
+ expect(template).not.toContain('.Scripts.Register')
5748
+ })
5749
+
5750
+ // Regression guard: a previous attempt emitted a literal
5751
+ // `<link rel="modulepreload">` tag directly into the component template,
5752
+ // which injected a rendered DOM node before the component's root and
5753
+ // broke hydration across all eight integrations (blade, erb,
5754
+ // go-template, jinja, mojolicious, rust, twig, xslate). Preload hints
5755
+ // must ONLY ever be emitted as no-output register statements (here,
5756
+ // `{{.Scripts.RegisterPreload "..."}}`) that the adapter's runtime later
5757
+ // renders itself — never as literal markup baked into the template.
5758
+ test('never emits a literal <link tag into the template', () => {
5759
+ const ir = compileToIR(CLIENT_COMPONENT)
5760
+ const { template } = new GoTemplateAdapter().generate(ir, {
5761
+ scriptAssets: ['/assets/runtime-abc123.js'],
5762
+ preloadAssets: ['/assets/index-pre1.js'],
5763
+ })
5764
+ expect(template).not.toContain('<link')
5765
+ })
5766
+ })
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { combineGoTypes, deduplicateGoTypes } from '../build'
2
+ import { combineGoTypes, deduplicateGoTypes } from '../go-types'
3
3
 
4
4
  describe('combineGoTypes stdlib imports', () => {
5
5
  // The combined types file strips each component's own import block and
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Coverage of `@barefootjs/go-template/vite`'s `barefoot()`:
3
+ *
4
+ * - one real `vite build()` end to end (mirrors `packages/vite`'s own
5
+ * `e2e-vite-build.test.ts` rigor — a plugin that only passes mocked unit
6
+ * tests hasn't been shown to work) against a checked-in fixture (not a
7
+ * system tmpdir) so `@barefootjs/client` resolves through the
8
+ * monorepo's real node_modules symlinks;
9
+ * - the `afterEmit`-driven `components.go` combining behavior (empty
10
+ * `types`, write-if-changed) exercised by calling the returned plugin's
11
+ * own hooks directly, same style as `packages/vite`'s `plugin.test.ts` —
12
+ * `compileCanonical` never resolves `@barefootjs/client` as a real
13
+ * import (it's plain JSX→JS codegen, not a module load), so these don't
14
+ * need the fixture's real dependency graph at all.
15
+ */
16
+ import { describe, test, expect, afterEach } from 'bun:test'
17
+ import { build } from 'vite'
18
+ import { mkdtemp, rm, mkdir, writeFile, readFile, stat } from 'node:fs/promises'
19
+ import { tmpdir } from 'node:os'
20
+ import { join, resolve } from 'node:path'
21
+ import { barefoot, barefoot as defaultBarefoot } from '../vite.ts'
22
+
23
+ // biome-ignore lint: hooks are called directly, bypassing Vite's own
24
+ // dispatch/typing — the same cast `packages/vite/src/__tests__/
25
+ // plugin.test.ts` uses to unit-test a Vite plugin's hooks in isolation.
26
+ type AnyPlugin = any
27
+
28
+ const FIXTURE_ROOT = resolve(import.meta.dirname, '../../e2e-fixture')
29
+
30
+ describe('@barefootjs/go-template/vite: real vite build', () => {
31
+ afterEach(async () => {
32
+ await rm(join(FIXTURE_ROOT, 'components.go'), { force: true })
33
+ })
34
+
35
+ test('exports the same function as both named `barefoot` and default', () => {
36
+ expect(defaultBarefoot).toBe(barefoot)
37
+ })
38
+
39
+ test('returns a single-element plugin array when `assets` is omitted', () => {
40
+ const plugins = barefoot({ components: ['src/components'], templates: 'views' })
41
+ expect(plugins).toHaveLength(1)
42
+ })
43
+
44
+ // core's `barefoot()` attaches `api.options` (see `BarefootPluginApi`) to
45
+ // the SAME plugin object this wrapper returns unchanged as `plugins[0]`
46
+ // — this pins that composition doesn't lose it, for either array shape
47
+ // (`assets` omitted vs. present, which adds a SECOND, unrelated
48
+ // companion plugin alongside it).
49
+ test('surfaces core\'s plugin.api.options unchanged on the returned plugin, with the GoTemplateAdapter it constructed', () => {
50
+ const plugins = barefoot({ components: ['src/components', '../shared/blog'], templates: 'views' }) as any[]
51
+ const core = plugins[0]
52
+ expect(core.name).toBe('barefoot')
53
+ expect(core.api.options.components).toEqual(['src/components', '../shared/blog'])
54
+ expect(core.api.options.templates).toBe('views')
55
+ expect(core.api.options.adapter.constructor.name).toBe('GoTemplateAdapter')
56
+ })
57
+
58
+ test('still surfaces api.options on plugins[0] when `assets` adds a second companion plugin', () => {
59
+ const plugins = barefoot({
60
+ components: ['src/components'],
61
+ templates: 'views',
62
+ assets: { Bootstrap: 'client/bootstrap.ts' },
63
+ }) as any[]
64
+ expect(plugins).toHaveLength(2)
65
+ expect(plugins[0].api.options.components).toEqual(['src/components'])
66
+ })
67
+
68
+ test('writes a compilable combined components.go after `vite build`, using the SAME combineGoTypes as ./build', async () => {
69
+ const outDir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-dist-'))
70
+ const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-views-'))
71
+
72
+ try {
73
+ await build({
74
+ configFile: false,
75
+ root: FIXTURE_ROOT,
76
+ base: '/static/build/',
77
+ logLevel: 'warn',
78
+ build: { outDir, emptyOutDir: true },
79
+ plugins: [
80
+ barefoot({
81
+ components: ['src/components'],
82
+ templates: templatesDir,
83
+ packageName: 'main',
84
+ typesOutputFile: 'components.go',
85
+ }),
86
+ ],
87
+ })
88
+
89
+ // Template got its scriptAssets baked in, same as core alone would do.
90
+ const template = await readFile(join(templatesDir, 'Counter.tmpl'), 'utf8')
91
+ expect(template).toContain('Scripts.Register')
92
+
93
+ // components.go exists, is combined (one package header, randomID
94
+ // defined), and compiles-shaped (no per-component leftover headers).
95
+ const componentsGo = await readFile(join(FIXTURE_ROOT, 'components.go'), 'utf8')
96
+ expect(componentsGo).toContain('package main')
97
+ expect(componentsGo).toContain('func randomID(n int) string {')
98
+ expect(componentsGo).toContain('CounterProps')
99
+ expect(componentsGo.match(/^package main$/gm)).toHaveLength(1)
100
+ } finally {
101
+ await rm(outDir, { recursive: true, force: true })
102
+ await rm(templatesDir, { recursive: true, force: true })
103
+ }
104
+ }, 60_000)
105
+
106
+ test('`assets` resolves a non-component entry\'s manifest-hashed URL into the `production`-tagged, gitignored sibling file', async () => {
107
+ const outDir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-dist-assets-'))
108
+ const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-views-assets-'))
109
+ // A real `build()` is the `mode: 'build'` pass, so this writes the
110
+ // PRODUCTION-tagged sibling (`bf_assets_prod.go`), not the dev-tagged
111
+ // `bf_assets.go` default filename — see writeAssetMap's mode branch.
112
+ const assetsGoPath = join(FIXTURE_ROOT, 'bf_assets_prod.go')
113
+
114
+ try {
115
+ await build({
116
+ configFile: false,
117
+ root: FIXTURE_ROOT,
118
+ base: '/static/build/',
119
+ logLevel: 'warn',
120
+ build: {
121
+ outDir,
122
+ emptyOutDir: true,
123
+ // Registering the non-component entry is the CALLER's job (stock
124
+ // Vite config) — `assets` below only resolves the URL Vite
125
+ // already bundled it to, it doesn't request the bundling.
126
+ rollupOptions: { input: { bootstrap: resolve(FIXTURE_ROOT, 'client/bootstrap.ts') } },
127
+ },
128
+ plugins: barefoot({
129
+ components: ['src/components'],
130
+ templates: templatesDir,
131
+ assets: { Bootstrap: 'client/bootstrap.ts' },
132
+ }),
133
+ })
134
+
135
+ const manifest = JSON.parse(await readFile(join(outDir, '.vite/manifest.json'), 'utf8'))
136
+ const expectedUrl = `/static/build/${manifest['client/bootstrap.ts'].file}`
137
+
138
+ const content = await readFile(assetsGoPath, 'utf8')
139
+ expect(content).toContain('//go:build production')
140
+ expect(content).toContain('package main')
141
+ expect(content).toContain(`"Bootstrap": ${JSON.stringify(expectedUrl)}`)
142
+
143
+ // The dev-tagged default filename must NOT be touched by a build pass
144
+ // — that file only gets (re)written by the dev pass, and a stray
145
+ // build-triggered write to it would defeat its "stable, safe to
146
+ // commit" property (see the module docstring).
147
+ await expect(readFile(join(FIXTURE_ROOT, 'bf_assets.go'), 'utf8')).rejects.toThrow()
148
+ } finally {
149
+ await rm(outDir, { recursive: true, force: true })
150
+ await rm(templatesDir, { recursive: true, force: true })
151
+ await rm(assetsGoPath, { force: true })
152
+ }
153
+ }, 60_000)
154
+
155
+ test('`assets` throws an actionable error when the entry was never registered as a Rollup input', async () => {
156
+ const outDir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-dist-assets-missing-'))
157
+ const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-views-assets-missing-'))
158
+
159
+ try {
160
+ await expect(
161
+ build({
162
+ configFile: false,
163
+ root: FIXTURE_ROOT,
164
+ base: '/static/build/',
165
+ logLevel: 'silent',
166
+ build: { outDir, emptyOutDir: true },
167
+ plugins: barefoot({
168
+ components: ['src/components'],
169
+ templates: templatesDir,
170
+ assets: { Bootstrap: 'client/bootstrap.ts' },
171
+ }),
172
+ }),
173
+ ).rejects.toThrow(/was not found in the build manifest/)
174
+ } finally {
175
+ await rm(outDir, { recursive: true, force: true })
176
+ await rm(templatesDir, { recursive: true, force: true })
177
+ await rm(join(FIXTURE_ROOT, 'bf_assets_prod.go'), { force: true })
178
+ }
179
+ }, 60_000)
180
+ })
181
+
182
+ describe('@barefootjs/go-template/vite: afterEmit → components.go, via direct hook calls', () => {
183
+ let dir: string
184
+
185
+ afterEach(async () => {
186
+ if (dir) await rm(dir, { recursive: true, force: true })
187
+ })
188
+
189
+ async function setup(): Promise<{ plugin: AnyPlugin; templatesDir: string; componentsGoPath: string }> {
190
+ dir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-hooks-'))
191
+ await mkdir(join(dir, 'src/components'), { recursive: true })
192
+ await writeFile(
193
+ join(dir, 'src/components/Counter.tsx'),
194
+ '\'use client\'\nimport { createSignal } from \'@barefootjs/client\'\nexport function Counter(props: { initial: number }) {\n const [count] = createSignal(props.initial)\n return <button>{count()}</button>\n}\n',
195
+ )
196
+
197
+ const templatesDir = join(dir, 'internal/views')
198
+ const plugin: AnyPlugin = barefoot({ components: ['src/components'], templates: templatesDir })[0]
199
+ await plugin.config({ root: dir }, { command: 'build', mode: 'production' })
200
+ plugin.configResolved({ root: dir, base: '/static/build/', build: { outDir: 'dist', manifest: true } })
201
+
202
+ await mkdir(join(dir, 'dist/.vite'), { recursive: true })
203
+ await writeFile(
204
+ join(dir, 'dist/.vite/manifest.json'),
205
+ JSON.stringify({ 'src/components/Counter.tsx': { file: 'assets/Counter-abc123.js', isEntry: true } }),
206
+ )
207
+
208
+ return { plugin, templatesDir, componentsGoPath: join(dir, 'components.go') }
209
+ }
210
+
211
+ test('combines the discovered component\'s types into components.go', async () => {
212
+ const { plugin, componentsGoPath } = await setup()
213
+ await plugin.writeBundle()
214
+
215
+ const content = await readFile(componentsGoPath, 'utf8')
216
+ expect(content).toContain('package main')
217
+ expect(content).toContain('CounterProps')
218
+ expect(content).toContain('func randomID(n int) string {')
219
+ })
220
+
221
+ test('does not write components.go when no discovered component produces `types` (ctx.types.size === 0)', async () => {
222
+ dir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-hooks-notypes-'))
223
+ await mkdir(join(dir, 'src/components'), { recursive: true }) // deliberately empty
224
+
225
+ const templatesDir = join(dir, 'internal/views')
226
+ const plugin: AnyPlugin = barefoot({ components: ['src/components'], templates: templatesDir })[0]
227
+ await plugin.config({ root: dir }, { command: 'build', mode: 'production' })
228
+ plugin.configResolved({ root: dir, base: '/', build: { outDir: 'dist', manifest: true } })
229
+ await mkdir(join(dir, 'dist/.vite'), { recursive: true })
230
+ await writeFile(join(dir, 'dist/.vite/manifest.json'), '{}')
231
+
232
+ await plugin.writeBundle()
233
+
234
+ await expect(readFile(join(dir, 'components.go'), 'utf8')).rejects.toThrow()
235
+ })
236
+
237
+ test('write-if-changed: a second pass with identical output does not rewrite components.go', async () => {
238
+ const { plugin, componentsGoPath } = await setup()
239
+
240
+ await plugin.writeBundle()
241
+ const firstMtime = (await stat(componentsGoPath)).mtimeMs
242
+
243
+ await new Promise(r => setTimeout(r, 20))
244
+ await plugin.writeBundle()
245
+ const secondMtime = (await stat(componentsGoPath)).mtimeMs
246
+
247
+ expect(secondMtime).toBe(firstMtime)
248
+ })
249
+
250
+ test('honors manualTypes and transformTypes', async () => {
251
+ dir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-hooks-manual-'))
252
+ await mkdir(join(dir, 'src/components'), { recursive: true })
253
+ await writeFile(join(dir, 'src/components/Greeting.tsx'), 'export function Greeting() { return <p>Hi</p> }\n')
254
+
255
+ const templatesDir = join(dir, 'internal/views')
256
+ const plugin: AnyPlugin = barefoot({
257
+ components: ['src/components'],
258
+ templates: templatesDir,
259
+ // manualTypes is appended verbatim, AFTER transformTypes runs on the
260
+ // component-derived content (see `combineGoTypes`), so app-specific
261
+ // hand-written types are never mangled by a transform meant for
262
+ // generated code.
263
+ manualTypes: '// app-specific hand-written type\ntype AppOnly struct{}',
264
+ transformTypes: types => types.replace(/GreetingProps/g, 'GreetingPropsRenamed'),
265
+ })[0]
266
+ await plugin.config({ root: dir }, { command: 'build', mode: 'production' })
267
+ plugin.configResolved({ root: dir, base: '/', build: { outDir: 'dist', manifest: true } })
268
+ await mkdir(join(dir, 'dist/.vite'), { recursive: true })
269
+ await writeFile(join(dir, 'dist/.vite/manifest.json'), '{}')
270
+
271
+ await plugin.writeBundle()
272
+
273
+ const content = await readFile(join(dir, 'components.go'), 'utf8')
274
+ expect(content).toContain('type AppOnly struct{}')
275
+ expect(content).toContain('GreetingPropsRenamed')
276
+ expect(content).not.toMatch(/\btype GreetingProps struct/)
277
+ })
278
+ })
@@ -189,7 +189,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
189
189
  // Sentinel marking a parent-scope `bf-s` slot inside a hoisted-JSX children
190
190
  // bake (see `extractScopedHtmlChildren`). Can't appear in real HTML text.
191
191
  private static readonly SCOPE_SENTINEL = '__BF_SCOPE_SENTINEL__'
192
- importMapInjection = 'html-snippet' as const
193
192
 
194
193
  // `renderFilterExpr` recursion state. `filterExprDepth` lets the outer call
195
194
  // reset `filterExprUnsupported` per independent filter expression; the flag,
@@ -492,7 +491,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
492
491
 
493
492
  const scriptRegistrations = options?.skipScriptRegistration
494
493
  ? ''
495
- : this.generateScriptRegistrations(ir, options?.scriptBaseName)
494
+ : this.generateScriptRegistrations(ir, options?.scriptBaseName, options?.scriptAssets, options?.preloadAssets)
496
495
 
497
496
  let template = `{{define "${this.state.componentName}"}}\n${scriptRegistrations}${templateBody}\n{{end}}\n`
498
497
  // Companion children defines execute with the parent's data via `bf_tmpl`.
@@ -611,11 +610,37 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
611
610
  }
612
611
 
613
612
  /**
614
- * Script registration code for the template start. Reads `.Scripts` (on every
615
- * Props struct) and guards the registrations with `{{if .Scripts}}` for a nil
616
- * collector.
613
+ * Script (and modulepreload) registration code for the template start.
614
+ * Reads `.Scripts` (on every Props struct) and guards the registrations
615
+ * with `{{if .Scripts}}` for a nil collector.
617
616
  */
618
- private generateScriptRegistrations(ir: ComponentIR, scriptBaseName?: string): string {
617
+ private generateScriptRegistrations(
618
+ ir: ComponentIR,
619
+ scriptBaseName?: string,
620
+ scriptAssets?: string[],
621
+ preloadAssets?: string[],
622
+ ): string {
623
+ // `scriptAssets`, when present (including `[]`), fully supersedes the
624
+ // adapter-computed `barefootJsPath` / `clientJsBasePath` fallback pair
625
+ // below — see `AdapterGenerateOptions.scriptAssets`. The caller (e.g.
626
+ // the Vite plugin) has already decided the exact ordered URL list,
627
+ // including whether any script is needed at all.
628
+ if (scriptAssets) {
629
+ if (scriptAssets.length === 0) return ''
630
+ // `preloadAssets` is only meaningful alongside a non-empty
631
+ // `scriptAssets` (see `AdapterGenerateOptions.preloadAssets`), and
632
+ // every preload registration is emitted BEFORE every script
633
+ // registration — a hint that arrives after the script it describes
634
+ // is useless. `RegisterPreload`, like `Register`, is a no-output
635
+ // statement: the `<link rel="modulepreload">` tag itself is only
636
+ // ever rendered by `BfScripts` (runtime/bf.go), never here.
637
+ const preloadRegistrations = (preloadAssets ?? []).map(
638
+ (url) => `{{.Scripts.RegisterPreload "${url}"}}`,
639
+ )
640
+ const registrations = scriptAssets.map((url) => `{{.Scripts.Register "${url}"}}`)
641
+ return `{{if .Scripts}}${preloadRegistrations.join('')}${registrations.join('')}{{end}}\n`
642
+ }
643
+
619
644
  const hasInteractivity = hasClientInteractivity(ir)
620
645
 
621
646
  if (!hasInteractivity) {
@@ -1,21 +1,5 @@
1
- // Go template build config factory for barefoot.config.ts
2
-
3
- import type { BuildOptions, PostBuildContext } from '@barefootjs/jsx'
4
- import { GoTemplateAdapter } from './adapter/index.ts'
5
- import type { GoTemplateAdapterOptions } from './adapter/index.ts'
6
-
7
- export interface GoTemplateBuildOptions extends BuildOptions {
8
- /** Adapter-specific options passed to GoTemplateAdapter */
9
- adapterOptions?: GoTemplateAdapterOptions
10
- /** Output path for combined Go types file (relative to projectDir, default: 'components.go') */
11
- typesOutputFile?: string
12
- /** Transform the combined types string before writing (for app-specific type fixes) */
13
- transformTypes?: (types: string) => string
14
- /** Manual type definitions to append (app-specific types not generated from components) */
15
- manualTypes?: string
16
- }
17
-
18
- // ── Go type helpers ──────────────────────────────────────────────────────
1
+ // Go type-combination helpers shared by the Vite plugin's post-emit hook
2
+ // (`vite.ts`'s `combineGoTypes` call, writing `components.go`).
19
3
 
20
4
  /**
21
5
  * Strip Go package header and import block, returning only type definitions.
@@ -205,71 +189,3 @@ export function combineGoTypes(options: {
205
189
 
206
190
  return parts.join('\n') + '\n'
207
191
  }
208
-
209
- // ── Config factory ───────────────────────────────────────────────────────
210
-
211
- /**
212
- * Create a BarefootBuildConfig for Go html/template projects.
213
- *
214
- * Uses structural typing — does not import BarefootBuildConfig to avoid
215
- * circular dependency between @barefootjs/go-template and @barefootjs/cli.
216
- */
217
- export function createConfig(options: GoTemplateBuildOptions = {}) {
218
- const packageName = options.adapterOptions?.packageName ?? 'main'
219
- const typesOutputFile = options.typesOutputFile ?? 'components.go'
220
-
221
- const postBuild = async (ctx: PostBuildContext) => {
222
- if (ctx.types.size === 0) return
223
-
224
- const content = combineGoTypes({
225
- types: ctx.types,
226
- packageName,
227
- manualTypes: options.manualTypes,
228
- transformTypes: options.transformTypes,
229
- })
230
-
231
- if (content) {
232
- const { resolve } = await import('node:path')
233
- const { readFile, writeFile } = await import('node:fs/promises')
234
- const outPath = resolve(ctx.projectDir, typesOutputFile)
235
- // Write only when content changed so cache-hit builds don't trip the
236
- // dev-reload sentinel (ctx.markChanged) and trigger a spurious reload.
237
- // Use node:fs/promises (not Bun.*) so this hook runs under either
238
- // runtime — the published `barefoot` CLI bin starts via Node.
239
- const prev = await readFile(outPath, 'utf-8').catch(() => null)
240
- if (prev !== content) {
241
- await writeFile(outPath, content)
242
- ctx.markChanged?.()
243
- console.log(`Generated: ${typesOutputFile}`)
244
- }
245
- }
246
- }
247
-
248
- // Chain user's postBuild with Go types generation
249
- const userPostBuild = options.postBuild
250
- const combinedPostBuild = userPostBuild
251
- ? async (ctx: PostBuildContext) => {
252
- await postBuild(ctx)
253
- await userPostBuild(ctx)
254
- }
255
- : postBuild
256
-
257
- return {
258
- adapter: new GoTemplateAdapter(options.adapterOptions),
259
- paths: options.paths,
260
- components: options.components,
261
- outDir: options.outDir,
262
- minify: options.minify,
263
- contentHash: options.contentHash,
264
- externals: options.externals,
265
- externalsBasePath: options.externalsBasePath,
266
- bundleEntries: options.bundleEntries,
267
- localImportPrefixes: options.localImportPrefixes,
268
- outputLayout: options.outputLayout ?? {
269
- templates: 'templates',
270
- clientJs: 'client',
271
- runtime: 'client',
272
- },
273
- postBuild: combinedPostBuild,
274
- }
275
- }
@@ -8,7 +8,7 @@
8
8
  import { compileJSX } from '@barefootjs/jsx'
9
9
  import type { TemplateAdapter, ComponentIR, ParsedExpr } from '@barefootjs/jsx'
10
10
  import { GoTemplateAdapter } from './adapter/go-template-adapter.ts'
11
- import { deduplicateGoTypes } from './build.ts'
11
+ import { deduplicateGoTypes } from './go-types.ts'
12
12
  import { capitalizeFieldName, goFieldNameForKey, loopKeyToGoFieldPath } from './adapter/lib/go-naming.ts'
13
13
  import { findNestedComponents } from './adapter/analysis/component-tree.ts'
14
14
  import type { NestedComponentInfo } from './adapter/lib/types.ts'
@@ -258,7 +258,7 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
258
258
  }
259
259
 
260
260
  // Merge entry + sibling + child type blocks through the same
261
- // `deduplicateGoTypes` helper `bf build` uses. Duplicates arise in two
261
+ // `deduplicateGoTypes` helper `vite.ts`'s `combineGoTypes` uses. Duplicates arise in two
262
262
  // ways (#1896): a multi-component file emits its module-scope shared
263
263
  // types (a context-value struct, a data `type Payment = …`) once per
264
264
  // component IR — both across the entry source's own sibling exports